1- import signal ,tempfile
21
32import os
43import re
4+ from signal import SIGINT , SIGTERM
55import subprocess
66import sys
7+ import tempfile
78import time
89
910import irods
1011import irods .helpers
11- import irods .client_configuration as config
1212from irods .test import modules as test_modules
13- from irods .test .modules .test_auto_close_of_data_objects__issue_456 import (
14- auto_close_data_objects ,
15- )
1613
1714OBJECT_SIZE = 2 * 1024 ** 3
1815OBJECT_NAME = 'data_get_issue__722'
1916LOCAL_TEMPFILE_NAME = 'data_object_for_issue_722.dat'
2017
2118_clock_resolution = max (.01 , * (time .clock_getres (getattr (time ,symbol ))
2219 for symbol in dir (time ) if symbol .startswith ('CLOCK_' )))
23- def wait_till_true ( function ):
20+ def wait_till_true (function , timeout = None ):
21+ start_time = time .clock_gettime_ns (time .CLOCK_BOOTTIME )
2422 while not (truth_value := function ()):
23+ if timeout is not None and (time .clock_gettime_ns (time .CLOCK_BOOTTIME )- start_time )* 1e-9 > timeout :
24+ break
2525 time .sleep (_clock_resolution )
2626 return truth_value
2727
2828
29- def test (
30- # TODO - accept unittest testcase 'test_instance' param , replace assert's with test_instance.assert
31- ):
29+ def test (test_case , sigs = (SIGINT ,SIGTERM )):
3230 """Creates a child process executing a long get() and ensures the process can be
3331 terminated using SIGINT or SIGTERM.
3432 """
@@ -38,31 +36,44 @@ def test(
3836 # performs a lengthy data object "get" operation (see the main body of the script, below.)
3937 process = subprocess .Popen ([sys .executable , program ],
4038 stderr = subprocess .PIPE ,
41- stdout = subprocess .PIPE , text = True )
39+ stdout = subprocess .PIPE ,
40+ text = True )
4241
42+ # Wait for download process to reach the point of spawning data transfer threads. In Python 3.9+ versions
43+ # of the concurrent.futures module, these are nondaemon threads and will block the exit of the main thread
44+ # unless measures are taken (#722).
4345 localfile = process .stdout .readline ().strip ()
44- assert wait_till_true (lambda :os .path .exists (localfile ) and os .stat (localfile ).st_size > OBJECT_SIZE // 2 )
46+ test_case .assertTrue (wait_till_true (lambda :os .path .exists (localfile ) and os .stat (localfile ).st_size > OBJECT_SIZE // 2 ),
47+ "Parallel download from data_objects.get() probably experienced a fatal error before spawning auxiliary data transfer threads."
48+ )
4549
46- sig = signal .SIGINT
47- process .send_signal (sig )
48- class TestFailed (Exception ):pass
49- try :
50- assert process .wait (timeout = 15 ) == - sig
51- assert re .search ('KeyboardInterrupt' ,process .stderr .read ())
52- except subprocess .TimeoutExpired as timeout_exc :
53- raise TestFailed ("Non-daemon thread(s) probably prevented subprocess's main thread from exiting" ) from timeout_exc
54- return True
50+ for sig in sigs :
51+ # Interrupt the sub-process with the given signal.
52+ process .send_signal (sig )
53+ # Assert that this signal is what killed the sub-process, rather than a timed out process "wait" or a natural exit
54+ # due to misproper or incomplete handling of the signal.
55+ try :
56+ test_case .assertEqual (process .wait (timeout = 15 ), - sig )
57+ except subprocess .TimeoutExpired as timeout_exc :
58+ test_case .fail ("Sub-process timed out before exit. Non-daemon thread(s) probably prevented subprocess's main thread from exiting" )
59+ # Assert that in the case of SIGINT, the process registered a KeyboardInterrupt.
60+ if sig == SIGINT :
61+ test_case .assertTrue (re .search ('KeyboardInterrupt' ,process .stderr .read ()))
5562
5663
5764if __name__ == "__main__" :
65+ # These lines are run only if the module is launched as a process.
5866 session = irods .helpers .make_session ()
5967 hc = irods .helpers .home_collection (session )
6068 TESTFILE_FILL = b'_' * (1024 * 1024 )
6169 object_path = f'{ hc } /{ OBJECT_NAME } '
70+
71+ # Create the object to be downloaded.
6272 with session .data_objects .open (object_path ,'w' ) as f :
6373 for y in range (OBJECT_SIZE // len (TESTFILE_FILL )):
6474 f .write (TESTFILE_FILL )
6575 local_path = None
76+ # Establish where (ie absolute path) to place the downloaded file, i.e. the get() target.
6677 try :
6778 with tempfile .NamedTemporaryFile (prefix = 'local_file_issue_722.dat' , delete = True ) as t :
6879 local_path = t .name
@@ -71,8 +82,10 @@ class TestFailed(Exception):pass
7182 print (local_path )
7283 sys .stdout .flush ()
7384
85+ # "get" the object
7486 session .data_objects .get (object_path , local_path )
7587 finally :
88+ # Clean up, whether or not the download succeeded.
7689 if local_path is not None and os .path .exists (local_path ):
7790 os .unlink (local_path )
7891 if session .data_objects .exists (object_path ):
0 commit comments