Haskell'in braket işlevi neden yürütülebilir dosyalarda çalışıyor ancak testlerde temizlenemiyor?


10

Ben Haskell'ın çok garip bir davranış görüyorum bracketişlevi farklı davranıyor olmasına bağlı olduğu stack runveya stack testkullanılır.

Docker kapsayıcılarını oluşturmak ve temizlemek için iki iç içe parantez kullanılan aşağıdaki kodu göz önünde bulundurun:

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
                     )
          )

Bu ile çalıştırın stack runve kesildiğinde Ctrl+C, beklenen çıktıyı alıyorum:

Inside both brackets, sleeping!
^CInner release
container2
Done with inner release
Outer release
container1
Done with outer release

Ve her iki Docker kapsayıcısının da oluşturulduğunu ve kaldırıldığını doğrulayabilirim.

Ancak, bu aynı kodu bir sınama yapıştırın ve çalıştırırsanız stack test, ilk temizleme yalnızca (bir kısmı) gerçekleşir:

Inside both brackets, sleeping!
^CInner release
container2

Bu, bir Docker konteynerinin makinemde çalışmasına neden olur. Neler oluyor?


Yığın testi iplik kullanıyor mu?
Carl

1
Emin değilim. İlginç bir gerçeği fark ettim: Eğer derlenmiş gerçek test yürütülebilir dosyasını kazıp .stack-workdoğrudan çalıştırırsam, sorun olmaz. Sadece altında koşarken olur stack test.
tom

Neler olduğunu tahmin edebiliyorum ama yığın kullanmıyorum. Bu sadece davranışa dayalı bir tahmin. 1) stack testİşçilerin iş parçacıklarını testleri yürütmeye başlar. 2) SIGINT tutucusu ana ipliği öldürür. 3) Haskell programları, ana iş parçacığı ek bir iş parçasını yok sayarak sona erer. 2, GHC tarafından derlenen programlar için SIGINT'teki varsayılan davranıştır. 3 Haskell'de ipliklerin nasıl çalıştığıdır. 1 tam bir tahmindir.
Carl

Yanıtlar:


6

Kullandığınızda stack run, Yığın etkili bir şekildeexec denetimi yürütülebilir dosyaya aktarmak için sistem çağrısı ; bu nedenle, yürütülebilir dosyayı doğrudan kabuktan çalıştırdığınız gibi, yeni yürütülebilir dosyanın işlemi çalışan Stack sürecinin yerini alır. İşlem ağacının peşinde olduğu gibi stack run. Özellikle yürütülebilir dosyanın Bash kabuğunun doğrudan bir alt öğesi olduğuna dikkat edin. Daha kritik olarak, terminalin ön plan işlem grubunun (TPGID) 17996 olduğunu ve bu işlem grubundaki (PGID) tek işlemin süreç olduğunu unutmayın 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

Sonuç olarak, stack runkabuğun altından veya doğrudan kabından çalışan işlemi kesmek için Ctrl-C tuşlarına bastığınızda , SIGINT sinyali yalnızca bracket-test-exeişleme gönderilir. Bu, zaman uyumsuz birUserInterrupt istisna oluşturur. Yol şu durumlarda bracketçalışır:

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

işlenirken zaman uyumsuz bir istisna alır body , çalıştırır releaseve istisnayı yeniden yükseltir. Yuvalanmış bracketaramalarınızla bu, iç gövdeyi kesintiye uğratma, iç serbest bırakmayı işleme, dış gövdeyi kesmek için istisnayı yeniden yükseltme ve dış serbest bırakmayı işleme ve son olarak programı sonlandırmak için istisnayı yeniden yükseltme etkisine sahiptir. (Dış aşağıdaki işlem daha olsaydı bracketsenin içinde mainişlevi, bunlar idam edilmeyeceği.)

Öte yandan, kullandığınızda stack test, Stack withProcessWaityürütülebilir dosyayı işlemin bir alt süreci olarak başlatmak için kullanır stack test. Aşağıdaki işlem ağacında bracket-test-testbunun bir alt işlemi olduğunu unutmayın stack test. Kritik olarak, terminalin ön plan süreç grubu 18050'dir ve bu süreç grubu hemstack test süreci sürecibracket-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

Eğer terminalde Ctrl-C vurduğunda, SIGINT sinyali gönderilir tüm ikisi de çok terminalin önalan işlem grubunda süreçler stack testve bracket-test-testsinyal almak. bracket-test-testsinyali işlemeye ve yukarıda açıklanan sonlandırıcıları çalıştırmaya başlayacaktır. Bununla birlikte, burada bir yarış koşulu var, çünkü stack testkesintiye uğradığında, ortası withProcessWaitaşağı yukarı aşağıdaki gibi tanımlanıyor:

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

yani, bracketkesintiye uğradığında, stopProcessalt süreci SIGTERMsinyal göndererek sonlandıran çağrıyı yapar . Bunun SIGINTaksine, bu zaman uyumsuz bir istisna oluşturmaz. Çocuğu, genellikle herhangi bir finalizörü çalıştırmayı bitirmeden hemen sonlandırır.

Bu sorunu çözmek için özellikle kolay bir yol düşünemiyorum. Bir yolu tesisleri kullanmakSystem.Posix süreci kendi süreç grubuna koymak için :

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"

Şimdi Ctrl-C, SIGINT'in yalnızca bracket-test-testsürece teslim edilmesine neden olacaktır . Temizlenecek, orijinal ön plan süreç grubunustack test ve sonlanır. Bu, testin başarısız olmasına neden olur ve stack testçalışmaya devam eder.

Alternatif olarak SIGTERM, stack testişlem sona erdikten sonra bile temizleme işlemini gerçekleştirmek için alt işlemin üstesinden gelmek ve bu işlemi çalışmaya devam ettirmek olabilir . Kabuk istemine bakarken süreç arka planda temizlik yapacağından çirkin bir şey bu.


Detaylı cevap için teşekkürler! FYI Burada bu konuda bir Stack hata dosyaladım: github.com/commercialhaskell/stack/issues/5144 . Gerçek düzeltme , (veya benzer bir seçenek) seçeneğiyle stack testsüreçleri başlatmak için olacak gibi görünüyor . delegate_ctlcSystem.Process
tom
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.