Veo un comportamiento muy extraño en el que la bracket
función de Haskell se comporta de manera diferente dependiendo de si se usa stack run
o no stack test
.
Considere el siguiente código, donde se usan dos corchetes anidados para crear y limpiar contenedores 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
)
)
Cuando ejecuto esto con stack run
e interrumpo con Ctrl+C
, obtengo el resultado esperado:
Inside both brackets, sleeping!
^CInner release
container2
Done with inner release
Outer release
container1
Done with outer release
Y puedo verificar que ambos contenedores Docker se crean y luego se eliminan.
Sin embargo, si pego exactamente el mismo código en una prueba y ejecuto stack test
, solo (parte de) ocurre la primera limpieza:
Inside both brackets, sleeping!
^CInner release
container2
Esto da como resultado un contenedor Docker que se ejecuta en mi máquina. ¿Que esta pasando?
- Me he asegurado de que se
ghc-options
les pase exactamente lo mismo a ambos. - Repo de demostración completo aquí: https://github.com/thomasjm/bracket-issue
.stack-work
y lo ejecuto directamente, entonces el problema no ocurre. Solo sucede cuando se ejecuta debajostack test
.stack test
inicia subprocesos de trabajo para manejar pruebas. 2) el controlador SIGINT mata el hilo principal. 3) Los programas Haskell finalizan cuando el hilo principal lo hace, ignorando cualquier hilo adicional. 2 es el comportamiento predeterminado en SIGINT para programas compilados por GHC. 3 es cómo funcionan los hilos en Haskell. 1 es una suposición completa.Respuestas:
Cuando lo usa
stack run
, Stack usa efectivamente unaexec
llamada del sistema para transferir el control al ejecutable, por lo que el proceso para el nuevo ejecutable reemplaza el proceso de Stack en ejecución, como si ejecutara el ejecutable directamente desde el shell. Así es como se ve el árbol de procesosstack run
. Tenga en cuenta en particular que el ejecutable es un hijo directo del shell Bash. Más críticamente, tenga en cuenta que el grupo de procesos en primer plano del terminal (TPGID) es 17996, y el único proceso en ese grupo de procesos (PGID) es elbracket-test-exe
proceso.Como resultado, cuando presiona Ctrl-C para interrumpir el proceso que se ejecuta debajo
stack run
o directamente desde el shell, la señal SIGINT se entrega solo albracket-test-exe
proceso. Esto genera unaUserInterrupt
excepción asincrónica . La forma en quebracket
funciona, cuando:recibe una excepción asincrónica mientras procesa
body
, se ejecutarelease
y luego vuelve a generar la excepción. Con susbracket
llamadas anidadas , esto tiene el efecto de interrumpir el cuerpo interno, procesar la liberación interna, volver a generar la excepción para interrumpir el cuerpo externo y procesar la liberación externa, y finalmente volver a generar la excepción para finalizar el programa. (Si hubiera más acciones siguiendo lo externobracket
en sumain
función, no se ejecutarían).Por otro lado, cuando lo usa
stack test
, Stack lo utilizawithProcessWait
para iniciar el ejecutable como un proceso secundario delstack test
proceso. En el siguiente árbol de procesos, tenga en cuenta quebracket-test-test
es un proceso secundario destack test
. Críticamente, el grupo de procesos en primer plano del terminal es 18050, y ese grupo de procesos incluye tanto elstack test
proceso como elbracket-test-test
proceso.Al llegar a Ctrl-C en la terminal, la señal SIGINT se envía a todos los procesos en el grupo de procesos en primer plano de la terminal por lo tanto
stack test
ybracket-test-test
obtener la señal.bracket-test-test
comenzará a procesar la señal y ejecutará los finalizadores como se describe anteriormente. Sin embargo, hay una condición de carrera aquí porque cuandostack test
se interrumpe, está en el medio delwithProcessWait
cual se define más o menos de la siguiente manera:entonces, cuando
bracket
se interrumpe, llama, lostopProcess
que termina el proceso secundario enviándole laSIGTERM
señal. En contraposición aSIGINT
esto, esto no genera una excepción asincrónica. Simplemente termina al niño inmediatamente, generalmente antes de que pueda terminar de ejecutar los finalizadores.No puedo pensar en una forma particularmente fácil de solucionar esto. Una forma es utilizar las instalaciones
System.Posix
para colocar el proceso en su propio grupo de procesos:Ahora, Ctrl-C dará como resultado que SIGINT se entregue solo al
bracket-test-test
proceso. Se limpiará, restaurará el grupo de procesos en primer plano original para señalar elstack test
proceso y finalizará. Esto dará como resultado la falla de la prueba ystack test
seguirá ejecutándose.Una alternativa sería tratar de manejar
SIGTERM
y mantener el proceso secundario ejecutándose para realizar la limpieza, incluso una vez que elstack test
proceso haya finalizado. Esto es un poco feo ya que el proceso se limpiará en segundo plano mientras observa el indicador de shell.fuente
stack test
iniciar procesos con ladelegate_ctlc
opción desdeSystem.Process
(o algo similar).