हास्केल के ब्रैकेट फ़ंक्शन निष्पादन योग्य कार्यों में क्यों काम करता है लेकिन परीक्षणों में सफाई करने में विफल रहता है?


10

मैं एक बहुत ही अजीब व्यवहार देख रहा हूं, जहां हास्केल का bracketकार्य अलग-अलग व्यवहार कर रहा है stack runया नहीं, stack testयह निर्भर करता है।

निम्नलिखित कोड पर विचार करें, जहां डॉक कंटेनर को बनाने और साफ करने के लिए दो नेस्टेड ब्रैकेट का उपयोग किया जाता है:

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

और मैं यह सत्यापित कर सकता हूं कि दोनों डॉकटर कंटेनर बनाए जाते हैं और फिर हटा दिए जाते हैं।

हालाँकि, अगर मैं इस सटीक कोड को परीक्षण में शामिल करता हूं और चलाता हूं, तो stack testकेवल (केवल) पहला क्लीनअप होता है:

Inside both brackets, sleeping!
^CInner release
container2

यह मेरे मशीन पर चल रहे डॉकटर कंटेनर में परिणाम करता है। क्या चल रहा है?

  • मैंने यह सुनिश्चित किया है कि ghc-optionsदोनों को एक ही पास किया जाए।
  • पूर्ण प्रदर्शन रेपो यहां: https://github.com/thomasjm/bracket-issue

क्या स्टैक टेस्ट थ्रेड्स का उपयोग करता है?
कार्ल

1
मुझे यकीन नहीं है। मैंने एक दिलचस्प तथ्य पर ध्यान दिया: यदि मैं वास्तविक संकलित परीक्षण निष्पादन योग्य खोदता हूं .stack-workऔर इसे सीधे चलाता हूं , तो समस्या नहीं होती है। यह केवल तब होता है जब नीचे चल रहा हो stack test
टॉम

मैं अनुमान लगा सकता हूं कि क्या चल रहा है, लेकिन मैं स्टैक का उपयोग नहीं करता हूं। यह व्यवहार पर आधारित एक अनुमान मात्र है। 1) stack testपरीक्षणों को संभालने के लिए कार्यकर्ता सूत्र शुरू करता है। 2) SIGINT हैंडलर मुख्य धागे को मारता है। 3) हास्केल कार्यक्रम समाप्त होता है जब मुख्य धागा करता है, किसी भी अतिरिक्त धागे की अनदेखी करता है। 2 GHC द्वारा संकलित कार्यक्रमों के लिए SIGINT पर डिफ़ॉल्ट व्यवहार है। 3 हास्केल में धागे कैसे काम करते हैं। 1 एक पूर्ण अनुमान है।
कार्ल

जवाबों:


6

जब आप उपयोग करते हैं stack run, तो स्टैक प्रभावी रूप execसे निष्पादन योग्य को नियंत्रण स्थानांतरित करने के लिए एक सिस्टम कॉल का उपयोग करता है , इसलिए नए निष्पादन योग्य के लिए प्रक्रिया चल रही स्टैक प्रक्रिया को बदल देती है, जैसे कि आप शेल से सीधे निष्पादन योग्य चलाएंगे। यहाँ क्या प्रक्रिया पेड़ के बाद की तरह लग रहा है stack run। विशेष रूप से ध्यान दें कि निष्पादन योग्य बैश शेल का एक सीधा बच्चा है। अधिक गंभीर रूप से, ध्यान दें कि टर्मिनल का अग्रभूमि प्रक्रिया समूह (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, तो स्टैक 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/com Commercialhaskell/ stack/ issues / 5144 । ऐसा लगता है कि असली फिक्स (या कुछ इसी तरह) stack testके delegate_ctlcविकल्प के साथ प्रक्रियाओं को लॉन्च करने के लिए होगा System.Process
टॉम
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.