Чому функція дужок Haskell працює у виконуваних файлах, але не вдається очистити тести?


10

Я бачу дуже дивна поведінка , де Хаскелл bracketфункція поводиться по- різному в залежності від того 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 залишився працювати на моїй машині. Що відбувається?


Чи використовує тест стека нитки?
Карл

1
Я не впевнений. Я помітив один цікавий факт: якщо я викопаю фактично складений тестовий виконуваний файл .stack-workі запускаю його безпосередньо, то проблеми не станеться. Це трапляється лише під час бігу під stack test.
Tom

Я можу здогадатися, що відбувається, але я взагалі не використовую стек. Це лише здогадка, заснована на поведінці. 1) stack testзапускає робочі потоки для обробки тестів. 2) обробник SIGINT вбиває основну нитку. 3) Програми Haskell завершуються, коли робить основний потік, ігноруючи будь-які додаткові потоки. 2 - поведінка SIGINT за замовчуванням для програм, складених GHC. 3, як працюють нитки в Haskell. 1 - повна здогадка.
Карл

Відповіді:


6

Коли ви використовуєте stack run, Stack ефективно використовує execсистемний виклик для передачі керування виконуваному файлу, тому процес для нового виконуваного файлу замінює запущений процес Stack так само, як якщо б ви виконували виконуваний файл безпосередньо з оболонки. Ось як виглядає дерево процесів stack run. Зауважте, зокрема, що виконуваний файл є прямим дочірнім оболонкою Bash. Критичніше, зауважте, що група процесу переднього плану терміналу (TPGID) становить 17996, і єдиний процес у цій групі процесів (PGID) - це bracket-test-exeпроцес.

PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13816 13831 13831 13831 pts/3    17996 Ss    2001   0:00  |       \_ /bin/bash --noediting -i
13831 17996 17996 13831 pts/3    17996 Sl+   2001   0:00  |       |   \_ .../.stack-work/.../bracket-test-exe

Як результат, коли ви натискаєте Ctrl-C, щоб перервати процес, що працює або під stack runоболонкою, або безпосередньо, сигнал SIGINT подається лише до bracket-test-exeпроцесу. Це спричиняє асинхронний UserInterruptвиняток. Спосіб bracketпрацює, коли:

bracket
  acquire
  (\() -> release)
  (\() -> body)

отримує асинхронний виняток під час обробки body, він запускається releaseі потім повторно збільшує виняток. З вашими вкладеними bracketдзвінками це призводить до переривання внутрішнього корпусу, обробки внутрішнього випуску, повторного підняття винятку для переривання зовнішнього тіла та обробки зовнішнього випуску та, нарешті, повторного збільшення винятку для припинення програми. (Якби bracketу вашій mainфункції було більше дій за зовнішніми , вони не виконувалися б.)

З іншого боку, коли ви використовуєте stack test, Stack використовує withProcessWaitдля запуску виконуваного файлу як дочірнього stack testпроцесу. У наступному дереві процесів зауважте, що bracket-test-testце дочірній процес stack test. Критично важливим є те, що група переднього плану терміналу складає 18050, і ця група включає в себе і stack testпроцес, і bracket-test-testпроцес.

PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13816 13831 13831 13831 pts/3    18050 Ss    2001   0:00  |       \_ /bin/bash --noediting -i
13831 18050 18050 13831 pts/3    18050 Sl+   2001   0:00  |       |   \_ stack test
18050 18060 18050 13831 pts/3    18050 Sl+   2001   0:00  |       |       \_ .../.stack-work/.../bracket-test-test

Коли ви натискаєте Ctrl-C в терміналі, сигнал SIGINT надсилається всім процесам у групі процесу переднього плану терміналу, тому обидва stack testі bracket-test-testотримують сигнал. bracket-test-testпочне обробляти сигнал і запустити фіналізатори, як описано вище. Однак тут є умова гонки, тому що коли stack testвона переривається, вона в середині withProcessWaitвизначається більш-менш так:

withProcessWait config f =
  bracket
    (startProcess config)
    stopProcess
    (\p -> f p <* waitExitCode p)

тому, коли його bracketпереривають, він викликає, stopProcessщо припиняє дочірній процес, передаючи йому SIGTERMсигнал. На відміну від SIGINTцього, це не викликає асинхронного винятку. Він просто припиняє дитину негайно, як правило, перш ніж вона може закінчити будь-який фіналізатор.

Я не можу придумати особливо легкого способу обійти це. Один із способів - використовувати засоби, System.Posixщоб перевести процес у свою власну групу процесів:

main :: IO ()
main = do
  -- save old terminal foreground process group
  oldpgid <- getTerminalProcessGroupID (Fd 2)
  -- get our PID
  mypid <- getProcessID
  let -- put us in our own foreground process group
      handleInt  = setTerminalProcessGroupID (Fd 2) mypid >> createProcessGroupFor mypid
      -- restore the old foreground process gorup
      releaseInt = setTerminalProcessGroupID (Fd 2) oldpgid
  bracket
    (handleInt >> putStrLn "acquire")
    (\() -> threadDelay 1000000 >> putStrLn "release" >> releaseInt)
    (\() -> putStrLn "between" >> threadDelay 60000000)
  putStrLn "finished"

Тепер, Ctrl-C призведе до того, що SIGINT буде доставлений лише в bracket-test-testпроцес. Він очистить, відновить початкову групу процесу переднього плану, щоб вказати на stack testпроцес, і припинить. Це призведе до відмови тесту і stack testпросто продовжить працювати.

Альтернативою було б спробувати впоратися з SIGTERMдітьми і тримати процес запуску для очищення навіть після того, як stack testпроцес закінчився. Це некрасиво, оскільки процес буде чистим у фоновому режимі, коли ви дивитесь на підказку оболонки.


Дякуємо за детальну відповідь! FYI Я подав помилку про стек про це тут: github.com/commercehaskell/stack/isissue/5144 . Схоже, справжнє виправлення було б stack testзапускати процеси з delegate_ctlcопцією з System.Process(або чогось подібного).
Том
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.