ทำไมฟังก์ชั่นวงเล็บปีกกาของ Haskell จึงใช้งานได้ใน executables แต่ล้มเหลวในการทำความสะอาดในการทดสอบ?


10

ฉันเห็นพฤติกรรมแปลก ๆ ที่bracketหน้าที่ของ Haskell ทำงานแตกต่างกันไปขึ้นอยู่กับว่ามีการใช้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 ที่ยังคงทำงานบนเครื่องของฉัน เกิดอะไรขึ้น?

  • ฉันทำให้แน่ใจว่าสิ่งเดียวกันghc-optionsนั้นถูกส่งผ่านไปยังทั้งคู่
  • repo สาธิตเต็มรูปแบบที่นี่: https://github.com/thomasjm/bracket-issue

การทดสอบสแต็กใช้เธรดหรือไม่
คาร์ล

1
ฉันไม่แน่ใจ. ฉันสังเกตเห็นข้อเท็จจริงที่น่าสนใจข้อหนึ่ง: ถ้าฉันขุดการทดสอบจริงที่คอมไพล์แล้วภายใต้.stack-workรันและรันโดยตรงปัญหาจะไม่เกิดขึ้น stack testมันจะเกิดขึ้นเมื่อมีการทำงานภายใต้
ทอม

ฉันสามารถเดาได้ว่าเกิดอะไรขึ้น แต่ฉันไม่ได้ใช้กองซ้อนเลย มันเป็นเพียงการคาดเดาตามพฤติกรรม 1) stack testเริ่มเธรดผู้ปฏิบัติงานเพื่อจัดการทดสอบ 2) ตัวจัดการ SIGINT ฆ่าเธรดหลัก 3) โปรแกรม Haskell จะสิ้นสุดเมื่อเธรดหลักทำโดยไม่สนใจเธรดเพิ่มเติมใด ๆ 2 คือพฤติกรรมเริ่มต้นบน SIGINT สำหรับโปรแกรมที่รวบรวมโดย GHC 3 คือการทำงานของเธรดใน Haskell 1 เป็นการเดาที่สมบูรณ์
Carl

คำตอบ:


6

เมื่อคุณใช้stack runStack จะใช้การexecเรียกของระบบเพื่อถ่ายโอนการควบคุมไปยัง executable อย่างมีประสิทธิภาพดังนั้นกระบวนการสำหรับ executable ใหม่จะแทนที่กระบวนการ stack ที่กำลังทำงานอยู่ราวกับว่าคุณจะเรียกใช้ executable โดยตรงจาก shell stack runนี่คือสิ่งที่กระบวนการต้นไม้ดูเหมือนว่าหลังจากที่ โปรดสังเกตว่าไฟล์ที่เรียกใช้งานนั้นเป็นลูกโดยตรงของ Bash shell ยิ่งช่วงวิกฤตโปรดทราบว่ากลุ่มกระบวนการพื้นหน้า (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 testStack จะใช้withProcessWaitเพื่อเรียกใช้งาน executable เป็นกระบวนการลูกของ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จะเริ่มประมวลผลสัญญาณและใช้งาน finalizers ตามที่อธิบายไว้ข้างต้น อย่างไรก็ตามมีสภาพการแข่งขันที่นี่เพราะเมื่อstack testถูกขัดจังหวะมันอยู่ตรงกลางwithProcessWaitซึ่งมีการกำหนดมากหรือน้อยดังนี้:

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

ดังนั้นเมื่อbracketถูกขัดจังหวะมันจะเรียกstopProcessซึ่งยุติกระบวนการเด็กโดยส่งSIGTERMสัญญาณ ในข้อSIGINTจำกัด นี้ไม่ได้ยกข้อยกเว้นแบบอะซิงโครนัส มันเพียงแค่หยุดเด็กทันทีโดยทั่วไปก่อนที่มันจะเสร็จสิ้นการเรียกใช้ Finalizers ใด ๆ

ฉันไม่สามารถคิดถึงวิธีง่าย ๆ ในการแก้ไขปัญหานี้ วิธีหนึ่งคือการใช้สิ่งอำนวยความสะดวกใน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 ผมยื่นข้อผิดพลาด Stack เกี่ยวกับเรื่องนี้ที่นี่: github.com/commercialhaskell/stack/issues/5144 ดูเหมือนว่าการแก้ไขจริงจะเป็นการstack testเปิดใช้งานกระบวนการด้วยdelegate_ctlcตัวเลือกจากSystem.Process(หรือบางอย่างที่คล้ายกัน)
ทอม
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.