Skip to content

Commit 7e9eaad

Browse files
authored
Add test.support.busy_retry() (#93770)
Add busy_retry() and sleeping_retry() functions to test.support.
1 parent 4e9fa71 commit 7e9eaad

12 files changed

+186
-99
lines changed

Doc/library/test.rst

+45
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,51 @@ The :mod:`test.support` module defines the following constants:
413413

414414
The :mod:`test.support` module defines the following functions:
415415

416+
.. function:: busy_retry(timeout, err_msg=None, /, *, error=True)
417+
418+
Run the loop body until ``break`` stops the loop.
419+
420+
After *timeout* seconds, raise an :exc:`AssertionError` if *error* is true,
421+
or just stop the loop if *error* is false.
422+
423+
Example::
424+
425+
for _ in support.busy_retry(support.SHORT_TIMEOUT):
426+
if check():
427+
break
428+
429+
Example of error=False usage::
430+
431+
for _ in support.busy_retry(support.SHORT_TIMEOUT, error=False):
432+
if check():
433+
break
434+
else:
435+
raise RuntimeError('my custom error')
436+
437+
.. function:: sleeping_retry(timeout, err_msg=None, /, *, init_delay=0.010, max_delay=1.0, error=True)
438+
439+
Wait strategy that applies exponential backoff.
440+
441+
Run the loop body until ``break`` stops the loop. Sleep at each loop
442+
iteration, but not at the first iteration. The sleep delay is doubled at
443+
each iteration (up to *max_delay* seconds).
444+
445+
See :func:`busy_retry` documentation for the parameters usage.
446+
447+
Example raising an exception after SHORT_TIMEOUT seconds::
448+
449+
for _ in support.sleeping_retry(support.SHORT_TIMEOUT):
450+
if check():
451+
break
452+
453+
Example of error=False usage::
454+
455+
for _ in support.sleeping_retry(support.SHORT_TIMEOUT, error=False):
456+
if check():
457+
break
458+
else:
459+
raise RuntimeError('my custom error')
460+
416461
.. function:: is_resource_enabled(resource)
417462

418463
Return ``True`` if *resource* is enabled and available. The list of

Lib/test/_test_multiprocessing.py

+25-35
Original file line numberDiff line numberDiff line change
@@ -4313,18 +4313,13 @@ def test_shared_memory_cleaned_after_process_termination(self):
43134313
p.terminate()
43144314
p.wait()
43154315

4316-
deadline = time.monotonic() + support.LONG_TIMEOUT
4317-
t = 0.1
4318-
while time.monotonic() < deadline:
4319-
time.sleep(t)
4320-
t = min(t*2, 5)
4316+
err_msg = ("A SharedMemory segment was leaked after "
4317+
"a process was abruptly terminated")
4318+
for _ in support.sleeping_retry(support.LONG_TIMEOUT, err_msg):
43214319
try:
43224320
smm = shared_memory.SharedMemory(name, create=False)
43234321
except FileNotFoundError:
43244322
break
4325-
else:
4326-
raise AssertionError("A SharedMemory segment was leaked after"
4327-
" a process was abruptly terminated.")
43284323

43294324
if os.name == 'posix':
43304325
# Without this line it was raising warnings like:
@@ -5334,20 +5329,18 @@ def create_and_register_resource(rtype):
53345329
p.terminate()
53355330
p.wait()
53365331

5337-
deadline = time.monotonic() + support.LONG_TIMEOUT
5338-
while time.monotonic() < deadline:
5339-
time.sleep(.5)
5332+
err_msg = (f"A {rtype} resource was leaked after a process was "
5333+
f"abruptly terminated")
5334+
for _ in support.sleeping_retry(support.SHORT_TIMEOUT,
5335+
err_msg):
53405336
try:
53415337
_resource_unlink(name2, rtype)
53425338
except OSError as e:
53435339
# docs say it should be ENOENT, but OSX seems to give
53445340
# EINVAL
53455341
self.assertIn(e.errno, (errno.ENOENT, errno.EINVAL))
53465342
break
5347-
else:
5348-
raise AssertionError(
5349-
f"A {rtype} resource was leaked after a process was "
5350-
f"abruptly terminated.")
5343+
53515344
err = p.stderr.read().decode('utf-8')
53525345
p.stderr.close()
53535346
expected = ('resource_tracker: There appear to be 2 leaked {} '
@@ -5575,18 +5568,17 @@ def wait_proc_exit(self):
55755568
# but this can take a bit on slow machines, so wait a few seconds
55765569
# if there are other children too (see #17395).
55775570
join_process(self.proc)
5571+
55785572
start_time = time.monotonic()
5579-
t = 0.01
5580-
while len(multiprocessing.active_children()) > 1:
5581-
time.sleep(t)
5582-
t *= 2
5583-
dt = time.monotonic() - start_time
5584-
if dt >= 5.0:
5585-
test.support.environment_altered = True
5586-
support.print_warning(f"multiprocessing.Manager still has "
5587-
f"{multiprocessing.active_children()} "
5588-
f"active children after {dt} seconds")
5573+
for _ in support.sleeping_retry(5.0, error=False):
5574+
if len(multiprocessing.active_children()) <= 1:
55895575
break
5576+
else:
5577+
dt = time.monotonic() - start_time
5578+
support.environment_altered = True
5579+
support.print_warning(f"multiprocessing.Manager still has "
5580+
f"{multiprocessing.active_children()} "
5581+
f"active children after {dt:.1f} seconds")
55905582

55915583
def run_worker(self, worker, obj):
55925584
self.proc = multiprocessing.Process(target=worker, args=(obj, ))
@@ -5884,17 +5876,15 @@ def tearDownClass(cls):
58845876
# but this can take a bit on slow machines, so wait a few seconds
58855877
# if there are other children too (see #17395)
58865878
start_time = time.monotonic()
5887-
t = 0.01
5888-
while len(multiprocessing.active_children()) > 1:
5889-
time.sleep(t)
5890-
t *= 2
5891-
dt = time.monotonic() - start_time
5892-
if dt >= 5.0:
5893-
test.support.environment_altered = True
5894-
support.print_warning(f"multiprocessing.Manager still has "
5895-
f"{multiprocessing.active_children()} "
5896-
f"active children after {dt} seconds")
5879+
for _ in support.sleeping_retry(5.0, error=False):
5880+
if len(multiprocessing.active_children()) <= 1:
58975881
break
5882+
else:
5883+
dt = time.monotonic() - start_time
5884+
support.environment_altered = True
5885+
support.print_warning(f"multiprocessing.Manager still has "
5886+
f"{multiprocessing.active_children()} "
5887+
f"active children after {dt:.1f} seconds")
58985888

58995889
gc.collect() # do garbage collection
59005890
if cls.manager._number_of_objects() != 0:

Lib/test/fork_wait.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,8 @@ def test_wait(self):
5454
self.threads.append(thread)
5555

5656
# busy-loop to wait for threads
57-
deadline = time.monotonic() + support.SHORT_TIMEOUT
58-
while len(self.alive) < NUM_THREADS:
59-
time.sleep(0.1)
60-
if deadline < time.monotonic():
57+
for _ in support.sleeping_retry(support.SHORT_TIMEOUT, error=False):
58+
if len(self.alive) >= NUM_THREADS:
6159
break
6260

6361
a = sorted(self.alive.keys())

Lib/test/support/__init__.py

+76
Original file line numberDiff line numberDiff line change
@@ -2250,3 +2250,79 @@ def atfork_func():
22502250
pass
22512251
atfork_func.reference = ref_cycle
22522252
os.register_at_fork(before=atfork_func)
2253+
2254+
2255+
def busy_retry(timeout, err_msg=None, /, *, error=True):
2256+
"""
2257+
Run the loop body until "break" stops the loop.
2258+
2259+
After *timeout* seconds, raise an AssertionError if *error* is true,
2260+
or just stop if *error is false.
2261+
2262+
Example:
2263+
2264+
for _ in support.busy_retry(support.SHORT_TIMEOUT):
2265+
if check():
2266+
break
2267+
2268+
Example of error=False usage:
2269+
2270+
for _ in support.busy_retry(support.SHORT_TIMEOUT, error=False):
2271+
if check():
2272+
break
2273+
else:
2274+
raise RuntimeError('my custom error')
2275+
2276+
"""
2277+
if timeout <= 0:
2278+
raise ValueError("timeout must be greater than zero")
2279+
2280+
start_time = time.monotonic()
2281+
deadline = start_time + timeout
2282+
2283+
while True:
2284+
yield
2285+
2286+
if time.monotonic() >= deadline:
2287+
break
2288+
2289+
if error:
2290+
dt = time.monotonic() - start_time
2291+
msg = f"timeout ({dt:.1f} seconds)"
2292+
if err_msg:
2293+
msg = f"{msg}: {err_msg}"
2294+
raise AssertionError(msg)
2295+
2296+
2297+
def sleeping_retry(timeout, err_msg=None, /,
2298+
*, init_delay=0.010, max_delay=1.0, error=True):
2299+
"""
2300+
Wait strategy that applies exponential backoff.
2301+
2302+
Run the loop body until "break" stops the loop. Sleep at each loop
2303+
iteration, but not at the first iteration. The sleep delay is doubled at
2304+
each iteration (up to *max_delay* seconds).
2305+
2306+
See busy_retry() documentation for the parameters usage.
2307+
2308+
Example raising an exception after SHORT_TIMEOUT seconds:
2309+
2310+
for _ in support.sleeping_retry(support.SHORT_TIMEOUT):
2311+
if check():
2312+
break
2313+
2314+
Example of error=False usage:
2315+
2316+
for _ in support.sleeping_retry(support.SHORT_TIMEOUT, error=False):
2317+
if check():
2318+
break
2319+
else:
2320+
raise RuntimeError('my custom error')
2321+
"""
2322+
2323+
delay = init_delay
2324+
for _ in busy_retry(timeout, err_msg, error=error):
2325+
yield
2326+
2327+
time.sleep(delay)
2328+
delay = min(delay * 2, max_delay)

Lib/test/test__xxsubinterpreters.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,11 @@ def _wait_for_interp_to_run(interp, timeout=None):
4545
# run subinterpreter eariler than the main thread in multiprocess.
4646
if timeout is None:
4747
timeout = support.SHORT_TIMEOUT
48-
start_time = time.monotonic()
49-
deadline = start_time + timeout
50-
while not interpreters.is_running(interp):
51-
if time.monotonic() > deadline:
52-
raise RuntimeError('interp is not running')
53-
time.sleep(0.010)
48+
for _ in support.sleeping_retry(timeout, error=False):
49+
if interpreters.is_running(interp):
50+
break
51+
else:
52+
raise RuntimeError('interp is not running')
5453

5554

5655
@contextlib.contextmanager

Lib/test/test_concurrent_futures.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -256,12 +256,12 @@ def test_initializer(self):
256256
else:
257257
with self.assertRaises(BrokenExecutor):
258258
future.result()
259+
259260
# At some point, the executor should break
260-
t1 = time.monotonic()
261-
while not self.executor._broken:
262-
if time.monotonic() - t1 > 5:
263-
self.fail("executor not broken after 5 s.")
264-
time.sleep(0.01)
261+
for _ in support.sleeping_retry(5, "executor not broken"):
262+
if self.executor._broken:
263+
break
264+
265265
# ... and from this point submit() is guaranteed to fail
266266
with self.assertRaises(BrokenExecutor):
267267
self.executor.submit(get_init_status)

Lib/test/test_multiprocessing_main_handling.py

+11-14
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import sys
4141
import time
4242
from multiprocessing import Pool, set_start_method
43+
from test import support
4344
4445
# We use this __main__ defined function in the map call below in order to
4546
# check that multiprocessing in correctly running the unguarded
@@ -59,13 +60,11 @@ def f(x):
5960
results = []
6061
with Pool(5) as pool:
6162
pool.map_async(f, [1, 2, 3], callback=results.extend)
62-
start_time = time.monotonic()
63-
while not results:
64-
time.sleep(0.05)
65-
# up to 1 min to report the results
66-
dt = time.monotonic() - start_time
67-
if dt > 60.0:
68-
raise RuntimeError("Timed out waiting for results (%.1f sec)" % dt)
63+
64+
# up to 1 min to report the results
65+
for _ in support.sleeping_retry(60, "Timed out waiting for results"):
66+
if results:
67+
break
6968
7069
results.sort()
7170
print(start_method, "->", results)
@@ -86,19 +85,17 @@ def f(x):
8685
import sys
8786
import time
8887
from multiprocessing import Pool, set_start_method
88+
from test import support
8989
9090
start_method = sys.argv[1]
9191
set_start_method(start_method)
9292
results = []
9393
with Pool(5) as pool:
9494
pool.map_async(int, [1, 4, 9], callback=results.extend)
95-
start_time = time.monotonic()
96-
while not results:
97-
time.sleep(0.05)
98-
# up to 1 min to report the results
99-
dt = time.monotonic() - start_time
100-
if dt > 60.0:
101-
raise RuntimeError("Timed out waiting for results (%.1f sec)" % dt)
95+
# up to 1 min to report the results
96+
for _ in support.sleeping_retry(60, "Timed out waiting for results"):
97+
if results:
98+
break
10299
103100
results.sort()
104101
print(start_method, "->", results)

Lib/test/test_signal.py

+13-12
Original file line numberDiff line numberDiff line change
@@ -812,13 +812,14 @@ def test_itimer_virtual(self):
812812
signal.signal(signal.SIGVTALRM, self.sig_vtalrm)
813813
signal.setitimer(self.itimer, 0.3, 0.2)
814814

815-
start_time = time.monotonic()
816-
while time.monotonic() - start_time < 60.0:
815+
for _ in support.busy_retry(60.0, error=False):
817816
# use up some virtual time by doing real work
818817
_ = pow(12345, 67890, 10000019)
819818
if signal.getitimer(self.itimer) == (0.0, 0.0):
820-
break # sig_vtalrm handler stopped this itimer
821-
else: # Issue 8424
819+
# sig_vtalrm handler stopped this itimer
820+
break
821+
else:
822+
# bpo-8424
822823
self.skipTest("timeout: likely cause: machine too slow or load too "
823824
"high")
824825

@@ -832,13 +833,14 @@ def test_itimer_prof(self):
832833
signal.signal(signal.SIGPROF, self.sig_prof)
833834
signal.setitimer(self.itimer, 0.2, 0.2)
834835

835-
start_time = time.monotonic()
836-
while time.monotonic() - start_time < 60.0:
836+
for _ in support.busy_retry(60.0, error=False):
837837
# do some work
838838
_ = pow(12345, 67890, 10000019)
839839
if signal.getitimer(self.itimer) == (0.0, 0.0):
840-
break # sig_prof handler stopped this itimer
841-
else: # Issue 8424
840+
# sig_prof handler stopped this itimer
841+
break
842+
else:
843+
# bpo-8424
842844
self.skipTest("timeout: likely cause: machine too slow or load too "
843845
"high")
844846

@@ -1307,8 +1309,6 @@ def handler(signum, frame):
13071309
self.setsig(signal.SIGALRM, handler) # for ITIMER_REAL
13081310

13091311
expected_sigs = 0
1310-
deadline = time.monotonic() + support.SHORT_TIMEOUT
1311-
13121312
while expected_sigs < N:
13131313
# Hopefully the SIGALRM will be received somewhere during
13141314
# initial processing of SIGUSR1.
@@ -1317,8 +1317,9 @@ def handler(signum, frame):
13171317

13181318
expected_sigs += 2
13191319
# Wait for handlers to run to avoid signal coalescing
1320-
while len(sigs) < expected_sigs and time.monotonic() < deadline:
1321-
time.sleep(1e-5)
1320+
for _ in support.sleeping_retry(support.SHORT_TIMEOUT, error=False):
1321+
if len(sigs) >= expected_sigs:
1322+
break
13221323

13231324
# All ITIMER_REAL signals should have been delivered to the
13241325
# Python handler

0 commit comments

Comments
 (0)