Skip to content

Commit 7e9a09f

Browse files
committed
[_722] added test
1 parent 7ff4469 commit 7e9a09f

File tree

2 files changed

+103
-0
lines changed

2 files changed

+103
-0
lines changed

irods/test/data_obj_test.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2951,6 +2951,9 @@ def test_replica_truncate__issue_534(self):
29512951
if data_objs.exists(data_path):
29522952
data_objs.unlink(data_path, force=True)
29532953

2954+
def test_handling_of_termination_signals_during_multithread_get__issue_722(self):
2955+
from irods.test.modules.test_signal_handling_in_multithread_get import test as test__issue_722
2956+
test__issue_722(self)
29542957

29552958
if __name__ == "__main__":
29562959
# let the tests find the parent irods lib
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
2+
import os
3+
import re
4+
import signal
5+
import subprocess
6+
import sys
7+
import tempfile
8+
import time
9+
10+
import irods
11+
import irods.helpers
12+
from irods.test import modules as test_modules
13+
14+
OBJECT_SIZE = 2*1024**3
15+
OBJECT_NAME = 'data_get_issue__722'
16+
LOCAL_TEMPFILE_NAME = 'data_object_for_issue_722.dat'
17+
18+
19+
_clock_polling_interval = max(.01, time.clock_getres(time.CLOCK_BOOTTIME))
20+
21+
22+
def wait_till_true(function, timeout=None):
23+
start_time = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
24+
while not (truth_value := function()):
25+
if timeout is not None and (time.clock_gettime_ns(time.CLOCK_BOOTTIME)-start_time)*1e-9 > timeout:
26+
break
27+
time.sleep(_clock_polling_interval)
28+
return truth_value
29+
30+
31+
def test(test_case, signal_names = ("SIGTERM",#"SIGINT"
32+
)):
33+
"""Creates a child process executing a long get() and ensures the process can be
34+
terminated using SIGINT or SIGTERM.
35+
"""
36+
program = os.path.join(test_modules.__path__[0], os.path.basename(__file__))
37+
38+
for signal_name in signal_names:
39+
# Call into this same module as a command. This will initiate another Python process that
40+
# performs a lengthy data object "get" operation (see the main body of the script, below.)
41+
process = subprocess.Popen([sys.executable, program],
42+
stderr=subprocess.PIPE,
43+
stdout=subprocess.PIPE,
44+
text = True)
45+
46+
# Wait for download process to reach the point of spawning data transfer threads. In Python 3.9+ versions
47+
# of the concurrent.futures module, these are nondaemon threads and will block the exit of the main thread
48+
# unless measures are taken (#722).
49+
localfile = process.stdout.readline().strip()
50+
test_case.assertTrue(wait_till_true(lambda:os.path.exists(localfile) and os.stat(localfile).st_size > OBJECT_SIZE//2),
51+
"Parallel download from data_objects.get() probably experienced a fatal error before spawning auxiliary data transfer threads."
52+
)
53+
54+
signal_message_info = f"While testing signal {signal_name}"
55+
sig = getattr(signal, signal_name)
56+
57+
# Interrupt the subprocess with the given signal.
58+
process.send_signal(sig)
59+
# Assert that this signal is what killed the subprocess, rather than a timed out process "wait" or a natural exit
60+
# due to misproper or incomplete handling of the signal.
61+
try:
62+
test_case.assertEqual(process.wait(timeout = 15), -sig, "{signal_message_info}: unexpected subprocess return code.")
63+
except subprocess.TimeoutExpired as timeout_exc:
64+
test_case.fail(f"{signal_message_info}: subprocess timed out before terminating. "
65+
"Non-daemon thread(s) probably prevented subprocess's main thread from exiting.")
66+
# Assert that in the case of SIGINT, the process registered a KeyboardInterrupt.
67+
if sig == signal.SIGINT:
68+
test_case.assertTrue(re.search('KeyboardInterrupt', process.stderr.read()),
69+
"{signal_message_info}: Expected 'KeyboardInterrupt' in log output.")
70+
71+
72+
if __name__ == "__main__":
73+
# These lines are run only if the module is launched as a process.
74+
session = irods.helpers.make_session()
75+
hc = irods.helpers.home_collection(session)
76+
TESTFILE_FILL = b'_'*(1024*1024)
77+
object_path = f'{hc}/{OBJECT_NAME}'
78+
79+
# Create the object to be downloaded.
80+
with session.data_objects.open(object_path,'w') as f:
81+
for y in range(OBJECT_SIZE//len(TESTFILE_FILL)):
82+
f.write(TESTFILE_FILL)
83+
local_path = None
84+
# Establish where (ie absolute path) to place the downloaded file, i.e. the get() target.
85+
try:
86+
with tempfile.NamedTemporaryFile(prefix='local_file_issue_722.dat', delete = True) as t:
87+
local_path = t.name
88+
89+
# Tell the parent process the name of the local file being "get"ted (got) from iRODS
90+
print(local_path)
91+
sys.stdout.flush()
92+
93+
# "get" the object
94+
session.data_objects.get(object_path, local_path)
95+
finally:
96+
# Clean up, whether or not the download succeeded.
97+
if local_path is not None and os.path.exists(local_path):
98+
os.unlink(local_path)
99+
if session.data_objects.exists(object_path):
100+
session.data_objects.unlink(object_path, force=True)

0 commit comments

Comments
 (0)