Je vois un comportement très étrange où la bracket
fonction de Haskell se comporte différemment selon qu'elle est utilisée stack run
ou non stack test
.
Considérez le code suivant, où deux crochets imbriqués sont utilisés pour créer et nettoyer les conteneurs 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
)
)
Lorsque j'exécute cela avec stack run
et que j'interromps avec Ctrl+C
, j'obtiens la sortie attendue:
Inside both brackets, sleeping!
^CInner release
container2
Done with inner release
Outer release
container1
Done with outer release
Et je peux vérifier que les deux conteneurs Docker sont créés puis supprimés.
Cependant, si je colle exactement ce même code dans un test et que j'exécute stack test
, seul (une partie de) le premier nettoyage se produit:
Inside both brackets, sleeping!
^CInner release
container2
Il en résulte un conteneur Docker laissé en cours d'exécution sur ma machine. Que se passe-t-il?
- J'ai fait en sorte que les mêmes informations
ghc-options
soient transmises aux deux. - Repo de démonstration complet ici: https://github.com/thomasjm/bracket-issue
.stack-work
et l'exécute directement, le problème ne se produit pas. Cela ne se produit que lorsque vous courez sousstack test
.stack test
démarre les threads de travail pour gérer les tests. 2) le gestionnaire SIGINT tue le fil principal. 3) Les programmes Haskell se terminent lorsque le thread principal le fait, ignorant tous les threads supplémentaires. 2 est le comportement par défaut sur SIGINT pour les programmes compilés par GHC. 3 est la façon dont les threads fonctionnent dans Haskell. 1 est une supposition complète.Réponses:
Lorsque vous utilisez
stack run
, Stack utilise efficacement unexec
appel système pour transférer le contrôle à l'exécutable, de sorte que le processus du nouvel exécutable remplace le processus Stack en cours d'exécution, comme si vous exécutiez l'exécutable directement à partir du shell. Voici à quoi ressemble l'arbre de processusstack run
. Notez en particulier que l'exécutable est un enfant direct du shell Bash. Plus important encore, notez que le groupe de processus de premier plan du terminal (TPGID) est 17996, et le seul processus de ce groupe de processus (PGID) est lebracket-test-exe
processus.Par conséquent, lorsque vous appuyez sur Ctrl-C pour interrompre le processus en cours d'exécution sous
stack run
ou directement à partir du shell, le signal SIGINT n'est délivré qu'aubracket-test-exe
processus. Cela déclenche uneUserInterrupt
exception asynchrone . La façon dontbracket
fonctionne, quand:reçoit une exception asynchrone lors du traitement
body
, il s'exécuterelease
puis relance l'exception. Avec vosbracket
appels imbriqués , cela a pour effet d'interrompre le corps interne, de traiter la version interne, de sur-lever l'exception pour interrompre le corps externe et de traiter la version externe, et enfin de sur-lever l'exception pour terminer le programme. (S'il y avait plus d'actions suivant l'extérieurbracket
de votremain
fonction, elles ne seraient pas exécutées.)D'un autre côté, lorsque vous utilisez
stack test
, Stack utilisewithProcessWait
pour lancer l'exécutable en tant que processus enfant dustack test
processus. Dans l'arborescence de processus suivante, notez qu'ilbracket-test-test
s'agit d'un processus enfant destack test
. De manière critique, le groupe de processus de premier plan du terminal est 18050, et ce groupe de processus comprend à la fois lestack test
processus et lebracket-test-test
processus.Lorsque vous appuyez sur Ctrl-C dans le terminal, le signal SIGINT est envoyé à tous les processus du groupe de processus de premier plan du terminal afin d' obtenir les deux
stack test
etbracket-test-test
le signal.bracket-test-test
démarre le traitement du signal et exécute les finaliseurs comme décrit ci-dessus. Cependant, il y a une condition de concurrence ici parce que quandstack test
est interrompu, c'est au milieu de celui-ciwithProcessWait
qui est défini plus ou moins comme suit:ainsi, lorsque son
bracket
est interrompu, il appellestopProcess
ce qui termine le processus fils en lui envoyant leSIGTERM
signal. En contraste avecSIGINT
, cela ne déclenche pas d'exception asynchrone. Il met fin à l'enfant immédiatement, généralement avant qu'il ne puisse terminer l'exécution des finaliseurs.Je ne peux pas penser à un moyen particulièrement facile de contourner cela. Une façon consiste à utiliser les installations
System.Posix
pour placer le processus dans son propre groupe de processus:Maintenant, Ctrl-C entraînera la livraison de SIGINT uniquement au
bracket-test-test
processus. Il nettoiera, restaurera le groupe de processus de premier plan d'origine pour pointer vers lestack test
processus et se terminera. Cela entraînera l'échec du test etstack test
continuera simplement à fonctionner.Une alternative serait d'essayer de gérer
SIGTERM
et de maintenir le processus enfant en cours d'exécution pour effectuer le nettoyage, même une fois lestack test
processus terminé. C'est un peu moche car le processus se nettoiera en arrière-plan pendant que vous regardez l'invite du shell.la source
stack test
de lancer des processus avec l'delegate_ctlc
option deSystem.Process
(ou quelque chose de similaire).