3
3
__all__ = 'staggered_race' ,
4
4
5
5
import contextlib
6
- import typing
7
6
8
- from . import events
9
- from . import exceptions as exceptions_mod
10
7
from . import locks
11
8
from . import tasks
9
+ from . import taskgroups
12
10
11
+ class _Done (Exception ):
12
+ pass
13
13
14
- async def staggered_race (
15
- coro_fns : typing .Iterable [typing .Callable [[], typing .Awaitable ]],
16
- delay : typing .Optional [float ],
17
- * ,
18
- loop : events .AbstractEventLoop = None ,
19
- ) -> typing .Tuple [
20
- typing .Any ,
21
- typing .Optional [int ],
22
- typing .List [typing .Optional [Exception ]]
23
- ]:
14
+ async def staggered_race (coro_fns , delay ):
24
15
"""Run coroutines with staggered start times and take the first to finish.
25
16
26
17
This method takes an iterable of coroutine functions. The first one is
@@ -52,8 +43,6 @@ async def staggered_race(
52
43
delay: amount of time, in seconds, between starting coroutines. If
53
44
``None``, the coroutines will run sequentially.
54
45
55
- loop: the event loop to use.
56
-
57
46
Returns:
58
47
tuple *(winner_result, winner_index, exceptions)* where
59
48
@@ -72,37 +61,11 @@ async def staggered_race(
72
61
73
62
"""
74
63
# TODO: when we have aiter() and anext(), allow async iterables in coro_fns.
75
- loop = loop or events .get_running_loop ()
76
- enum_coro_fns = enumerate (coro_fns )
77
64
winner_result = None
78
65
winner_index = None
79
66
exceptions = []
80
- running_tasks = []
81
-
82
- async def run_one_coro (
83
- previous_failed : typing .Optional [locks .Event ]) -> None :
84
- # Wait for the previous task to finish, or for delay seconds
85
- if previous_failed is not None :
86
- with contextlib .suppress (exceptions_mod .TimeoutError ):
87
- # Use asyncio.wait_for() instead of asyncio.wait() here, so
88
- # that if we get cancelled at this point, Event.wait() is also
89
- # cancelled, otherwise there will be a "Task destroyed but it is
90
- # pending" later.
91
- await tasks .wait_for (previous_failed .wait (), delay )
92
- # Get the next coroutine to run
93
- try :
94
- this_index , coro_fn = next (enum_coro_fns )
95
- except StopIteration :
96
- return
97
- # Start task that will run the next coroutine
98
- this_failed = locks .Event ()
99
- next_task = loop .create_task (run_one_coro (this_failed ))
100
- running_tasks .append (next_task )
101
- assert len (running_tasks ) == this_index + 2
102
- # Prepare place to put this coroutine's exceptions if not won
103
- exceptions .append (None )
104
- assert len (exceptions ) == this_index + 1
105
67
68
+ async def run_one_coro (this_index , coro_fn , this_failed ):
106
69
try :
107
70
result = await coro_fn ()
108
71
except (SystemExit , KeyboardInterrupt ):
@@ -116,34 +79,17 @@ async def run_one_coro(
116
79
assert winner_index is None
117
80
winner_index = this_index
118
81
winner_result = result
119
- # Cancel all other tasks. We take care to not cancel the current
120
- # task as well. If we do so, then since there is no `await` after
121
- # here and CancelledError are usually thrown at one, we will
122
- # encounter a curious corner case where the current task will end
123
- # up as done() == True, cancelled() == False, exception() ==
124
- # asyncio.CancelledError. This behavior is specified in
125
- # https://bugs.python.org/issue30048
126
- for i , t in enumerate (running_tasks ):
127
- if i != this_index :
128
- t .cancel ()
129
-
130
- first_task = loop .create_task (run_one_coro (None ))
131
- running_tasks .append (first_task )
82
+ raise _Done
83
+
132
84
try :
133
- # Wait for a growing list of tasks to all finish: poor man's version of
134
- # curio's TaskGroup or trio's nursery
135
- done_count = 0
136
- while done_count != len (running_tasks ):
137
- done , _ = await tasks .wait (running_tasks )
138
- done_count = len (done )
139
- # If run_one_coro raises an unhandled exception, it's probably a
140
- # programming error, and I want to see it.
141
- if __debug__ :
142
- for d in done :
143
- if d .done () and not d .cancelled () and d .exception ():
144
- raise d .exception ()
145
- return winner_result , winner_index , exceptions
146
- finally :
147
- # Make sure no tasks are left running if we leave this function
148
- for t in running_tasks :
149
- t .cancel ()
85
+ async with taskgroups .TaskGroup () as tg :
86
+ for this_index , coro_fn in enumerate (coro_fns ):
87
+ this_failed = locks .Event ()
88
+ exceptions .append (None )
89
+ tg .create_task (run_one_coro (this_index , coro_fn , this_failed ))
90
+ with contextlib .suppress (TimeoutError ):
91
+ await tasks .wait_for (this_failed .wait (), delay )
92
+ except* _Done :
93
+ pass
94
+
95
+ return winner_result , winner_index , exceptions
0 commit comments