Veo un comportamiento muy extraño en el que la bracketfunción de Haskell se comporta de manera diferente dependiendo de si se usa stack runo 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 rune 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-optionsles pase exactamente lo mismo a ambos.
- Repo de demostración completo aquí: https://github.com/thomasjm/bracket-issue

.stack-worky lo ejecuto directamente, entonces el problema no ocurre. Solo sucede cuando se ejecuta debajostack test.stack testinicia 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 unaexecllamada 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-exeproceso.Como resultado, cuando presiona Ctrl-C para interrumpir el proceso que se ejecuta debajo
stack runo directamente desde el shell, la señal SIGINT se entrega solo albracket-test-exeproceso. Esto genera unaUserInterruptexcepción asincrónica . La forma en quebracketfunciona, cuando:recibe una excepción asincrónica mientras procesa
body, se ejecutareleasey luego vuelve a generar la excepción. Con susbracketllamadas 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 externobracketen sumainfunción, no se ejecutarían).Por otro lado, cuando lo usa
stack test, Stack lo utilizawithProcessWaitpara iniciar el ejecutable como un proceso secundario delstack testproceso. En el siguiente árbol de procesos, tenga en cuenta quebracket-test-testes 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 testproceso como elbracket-test-testproceso.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 testybracket-test-testobtener la señal.bracket-test-testcomenzará a procesar la señal y ejecutará los finalizadores como se describe anteriormente. Sin embargo, hay una condición de carrera aquí porque cuandostack testse interrumpe, está en el medio delwithProcessWaitcual se define más o menos de la siguiente manera:entonces, cuando
bracketse interrumpe, llama, lostopProcessque termina el proceso secundario enviándole laSIGTERMseñal. En contraposición aSIGINTesto, 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.Posixpara colocar el proceso en su propio grupo de procesos:Ahora, Ctrl-C dará como resultado que SIGINT se entregue solo al
bracket-test-testproceso. Se limpiará, restaurará el grupo de procesos en primer plano original para señalar elstack testproceso y finalizará. Esto dará como resultado la falla de la prueba ystack testseguirá ejecutándose.Una alternativa sería tratar de manejar
SIGTERMy mantener el proceso secundario ejecutándose para realizar la limpieza, incluso una vez que elstack testproceso haya finalizado. Esto es un poco feo ya que el proceso se limpiará en segundo plano mientras observa el indicador de shell.fuente
stack testiniciar procesos con ladelegate_ctlcopción desdeSystem.Process(o algo similar).