Я вижу очень странное поведение, когда bracket
функция Haskell ведет себя по-разному в зависимости от того, используется ли она 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. В чем дело?
- Я убедился, что то же самое
ghc-options
передается обоим. - Полное демонстрационное репо здесь: https://github.com/thomasjm/bracket-issue
.stack-work
и запускаю его напрямую, то проблема не возникает. Это происходит только при работе подstack test
.stack test
запускает рабочие потоки для обработки тестов. 2) обработчик SIGINT убивает основной поток. 3) Программы на Haskell завершаются, когда основной поток завершает работу, игнорируя любые дополнительные потоки. 2 - поведение по умолчанию в SIGINT для программ, скомпилированных GHC. 3, как работают потоки в Haskell. 1 - полное предположение.Ответы:
При использовании
stack run
Stack эффективно используетexec
системный вызов для передачи управления исполняемому файлу, поэтому процесс для нового исполняемого файла заменяет запущенный процесс Stack, как если бы вы запускали исполняемый файл непосредственно из оболочки. Вот как выглядит дерево процессов послеstack run
. В частности, обратите внимание, что исполняемый файл является прямым потомком оболочки Bash. Что еще более важно, обратите внимание, что приоритетная группа процессов терминала (TPGID) - 17996, и единственным процессом в этой группе процессов (PGID) являетсяbracket-test-exe
процесс.В результате, когда вы нажимаете Ctrl-C, чтобы прервать процесс, запущенный под
stack run
оболочкой или непосредственно из нее, сигнал SIGINT доставляется толькоbracket-test-exe
процессу. Это вызывает асинхронноеUserInterrupt
исключение. Способbracket
работает, когда:получает асинхронное исключение во время обработки
body
, запускаетсяrelease
и затем повторно вызывает исключение. С вашими вложеннымиbracket
вызовами это приводит к прерыванию внутреннего тела, обработке внутреннего выпуска, повторному вызову исключения для прерывания внешнего тела, обработке внешнего выпуска и, наконец, повторному вызову исключения для завершения программы. (Если быbracket
в вашейmain
функции было больше действий, следующих за внешним , они не были бы выполнены.)С другой стороны, когда вы используете
stack test
, Stack используетwithProcessWait
для запуска исполняемый файл как дочерний процессstack test
процесса. Обратите внимание, что в следующем дереве процессовbracket-test-test
это дочерний процессstack test
. Критически важно, что приоритетной группой процессов терминала является 18050, и эта группа процессов включает в себя какstack test
процесс, так иbracket-test-test
процесс.Когда вы нажимаете Ctrl-C в терминале, сигнал SIGINT отправляется всем процессам в группе процессов переднего плана терминала, так что оба
stack test
иbracket-test-test
получают сигнал.bracket-test-test
начнет обработку сигнала и запустит финализаторы, как описано выше. Тем не менее, здесь есть состояние гонки, потому что, когдаstack test
оно прервано, оно находится в середине,withProcessWait
которое определяется более или менее следующим образом:поэтому, когда
bracket
он прерывается, он вызывает,stopProcess
который завершает дочерний процесс, посылая емуSIGTERM
сигнал. В противоположностьSIGINT
этому, это не вызывает асинхронного исключения. Он просто немедленно завершает дочерний процесс, обычно до того, как он может завершить выполнение финализаторов.Я не могу придумать особенно простой способ обойти это. Одним из способов является использование средств
System.Posix
для помещения процесса в собственную группу процессов:Теперь Ctrl-C приведет к тому, что SIGINT будет доставлен только
bracket-test-test
процессу. Он очистит, восстановит исходную группу процессов переднего плана, чтобы указать наstack test
процесс, и завершится. Это приведет к сбою теста иstack test
продолжит работу.Альтернативой может быть попытка обработать
SIGTERM
и поддерживать дочерний процесс, выполняющий очистку, даже после завершенияstack test
процесса. Это немного уродливо, поскольку процесс будет как бы очищаться в фоновом режиме, пока вы смотрите на приглашение оболочки.источник
stack test
запуска процессов сdelegate_ctlc
опцией отSystem.Process
(или что-то подобное).