Estou vendo um comportamento muito estranho, onde a bracket
função de Haskell está se comportando de maneira diferente, dependendo de ser stack run
ou stack test
não usada.
Considere o código a seguir, onde dois colchetes aninhados são usados para criar e limpar contêineres do 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
)
)
Quando executo isso stack run
e o interrompo Ctrl+C
, obtenho a saída esperada:
Inside both brackets, sleeping!
^CInner release
container2
Done with inner release
Outer release
container1
Done with outer release
E posso verificar se os dois contêineres do Docker são criados e removidos.
No entanto, se eu colar exatamente o mesmo código em um teste e executar stack test
, apenas (parte de) a primeira limpeza acontece:
Inside both brackets, sleeping!
^CInner release
container2
Isso resulta em um contêiner do Docker em execução na minha máquina. O que está acontecendo?
- Eu me certifiquei de que exatamente o mesmo
ghc-options
seja passado para ambos. - Repo de demonstração completo aqui: https://github.com/thomasjm/bracket-issue
.stack-work
e executá-lo diretamente, o problema não ocorrerá. Isso só acontece quando executado sobstack test
.stack test
inicia threads de trabalho para manipular testes. 2) o manipulador SIGINT mata a rosca principal. 3) Os programas Haskell terminam quando o thread principal termina, ignorando quaisquer threads adicionais. 2 é o comportamento padrão no SIGINT para programas compilados pelo GHC. 3 é como os threads funcionam no Haskell. 1 é um palpite completo.Respostas:
Quando você usa
stack run
, o Stack efetivamente usa umaexec
chamada de sistema para transferir o controle para o executável; portanto, o processo para o novo executável substitui o processo de Stack em execução, como se você executasse o executável diretamente do shell. Aqui está a aparência da árvore de processostack run
. Observe em particular que o executável é um filho direto do shell Bash. Mais criticamente, observe que o grupo de processos em primeiro plano do terminal (TPGID) é 17996 e o único processo nesse grupo de processos (PGID) é obracket-test-exe
processo.Como resultado, quando você pressiona Ctrl-C para interromper o processo em execução sob
stack run
ou diretamente do shell, o sinal SIGINT é entregue apenas aobracket-test-exe
processo. Isso gera umaUserInterrupt
exceção assíncrona . A maneira comobracket
funciona, quando:recebe uma exceção assíncrona durante o processamento
body
, executarelease
e gera novamente a exceção. Com suasbracket
chamadas aninhadas , isso tem o efeito de interromper o corpo interno, processar a liberação interna, aumentar novamente a exceção para interromper o corpo externo e processar a liberação externa e, finalmente, aumentar novamente a exceção para encerrar o programa. (Se houvesse mais ações seguindo a parte externabracket
da suamain
função, elas não seriam executadas.)Por outro lado, quando você usa
stack test
, o Stack usawithProcessWait
para iniciar o executável como um processo filho dostack test
processo. Na árvore de processos a seguir, observe quebracket-test-test
é um processo filho destack test
. Criticamente, o grupo de processos em primeiro plano do terminal é 18050 e esse grupo de processos inclui ostack test
processo e obracket-test-test
processo.Quando você pressione Ctrl-C no terminal, o sinal SIGINT é enviado para todos os processos no grupo de processos de primeiro plano do terminal para ambos
stack test
ebracket-test-test
obter o sinal.bracket-test-test
começará a processar o sinal e executar os finalizadores conforme descrito acima. No entanto, há uma condição de corrida aqui porque, quandostack test
interrompida, está no meio dawithProcessWait
qual é definida mais ou menos da seguinte maneira:portanto, quando
bracket
é interrompido, ele chama ostopProcess
que encerra o processo filho enviando oSIGTERM
sinal. Em contraste comSIGINT
, isso não gera uma exceção assíncrona. Ele acaba com o filho imediatamente, geralmente antes de concluir a execução de qualquer finalizador.Não consigo pensar em uma maneira particularmente fácil de solucionar isso. Uma maneira é usar as instalações
System.Posix
para colocar o processo em seu próprio grupo de processos:Agora, Ctrl-C resultará na entrega do SIGINT apenas ao
bracket-test-test
processo. Ele limpará, restaurará o grupo de processos em primeiro plano original para apontar para ostack test
processo e terminar. Isso resultará na falha do teste estack test
continuará sendo executado.Uma alternativa seria tentar manipular
SIGTERM
e manter o processo filho em execução para executar a limpeza, mesmo após ostack test
término do processo. Isso é meio feio, pois o processo meio que se limpa em segundo plano enquanto você olha para o prompt do shell.fonte
stack test
iniciar processos com adelegate_ctlc
opção deSystem.Process
(ou algo semelhante).