Skip to content

Commit 133e929

Browse files
gh-124309: Revert eager task factory fix to prevent breaking downstream (#124810)
* Revert "GH-124639: add back loop param to staggered_race (#124700)" This reverts commit e0a41a5. * Revert "gh-124309: Modernize the `staggered_race` implementation to support eager task factories (#124390)" This reverts commit de929f3.
1 parent 7bdfabe commit 133e929

File tree

5 files changed

+65
-124
lines changed

5 files changed

+65
-124
lines changed

Lib/asyncio/base_events.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1144,7 +1144,7 @@ async def create_connection(
11441144
(functools.partial(self._connect_sock,
11451145
exceptions, addrinfo, laddr_infos)
11461146
for addrinfo in infos),
1147-
happy_eyeballs_delay)
1147+
happy_eyeballs_delay, loop=self)
11481148

11491149
if sock is None:
11501150
exceptions = [exc for sub in exceptions for exc in sub]

Lib/asyncio/staggered.py

+60-23
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@
44

55
import contextlib
66

7+
from . import events
8+
from . import exceptions as exceptions_mod
79
from . import locks
810
from . import tasks
9-
from . import taskgroups
1011

11-
class _Done(Exception):
12-
pass
1312

1413
async def staggered_race(coro_fns, delay, *, loop=None):
1514
"""Run coroutines with staggered start times and take the first to finish.
@@ -43,6 +42,8 @@ async def staggered_race(coro_fns, delay, *, loop=None):
4342
delay: amount of time, in seconds, between starting coroutines. If
4443
``None``, the coroutines will run sequentially.
4544
45+
loop: the event loop to use.
46+
4647
Returns:
4748
tuple *(winner_result, winner_index, exceptions)* where
4849
@@ -61,11 +62,36 @@ async def staggered_race(coro_fns, delay, *, loop=None):
6162
6263
"""
6364
# TODO: when we have aiter() and anext(), allow async iterables in coro_fns.
65+
loop = loop or events.get_running_loop()
66+
enum_coro_fns = enumerate(coro_fns)
6467
winner_result = None
6568
winner_index = None
6669
exceptions = []
70+
running_tasks = []
71+
72+
async def run_one_coro(previous_failed) -> None:
73+
# Wait for the previous task to finish, or for delay seconds
74+
if previous_failed is not None:
75+
with contextlib.suppress(exceptions_mod.TimeoutError):
76+
# Use asyncio.wait_for() instead of asyncio.wait() here, so
77+
# that if we get cancelled at this point, Event.wait() is also
78+
# cancelled, otherwise there will be a "Task destroyed but it is
79+
# pending" later.
80+
await tasks.wait_for(previous_failed.wait(), delay)
81+
# Get the next coroutine to run
82+
try:
83+
this_index, coro_fn = next(enum_coro_fns)
84+
except StopIteration:
85+
return
86+
# Start task that will run the next coroutine
87+
this_failed = locks.Event()
88+
next_task = loop.create_task(run_one_coro(this_failed))
89+
running_tasks.append(next_task)
90+
assert len(running_tasks) == this_index + 2
91+
# Prepare place to put this coroutine's exceptions if not won
92+
exceptions.append(None)
93+
assert len(exceptions) == this_index + 1
6794

68-
async def run_one_coro(this_index, coro_fn, this_failed):
6995
try:
7096
result = await coro_fn()
7197
except (SystemExit, KeyboardInterrupt):
@@ -79,23 +105,34 @@ async def run_one_coro(this_index, coro_fn, this_failed):
79105
assert winner_index is None
80106
winner_index = this_index
81107
winner_result = result
82-
raise _Done
83-
108+
# Cancel all other tasks. We take care to not cancel the current
109+
# task as well. If we do so, then since there is no `await` after
110+
# here and CancelledError are usually thrown at one, we will
111+
# encounter a curious corner case where the current task will end
112+
# up as done() == True, cancelled() == False, exception() ==
113+
# asyncio.CancelledError. This behavior is specified in
114+
# https://bugs.python.org/issue30048
115+
for i, t in enumerate(running_tasks):
116+
if i != this_index:
117+
t.cancel()
118+
119+
first_task = loop.create_task(run_one_coro(None))
120+
running_tasks.append(first_task)
84121
try:
85-
tg = taskgroups.TaskGroup()
86-
# Intentionally override the loop in the TaskGroup to avoid
87-
# using the running loop, preserving backwards compatibility
88-
# TaskGroup only starts using `_loop` after `__aenter__`
89-
# so overriding it here is safe.
90-
tg._loop = loop
91-
async with tg:
92-
for this_index, coro_fn in enumerate(coro_fns):
93-
this_failed = locks.Event()
94-
exceptions.append(None)
95-
tg.create_task(run_one_coro(this_index, coro_fn, this_failed))
96-
with contextlib.suppress(TimeoutError):
97-
await tasks.wait_for(this_failed.wait(), delay)
98-
except* _Done:
99-
pass
100-
101-
return winner_result, winner_index, exceptions
122+
# Wait for a growing list of tasks to all finish: poor man's version of
123+
# curio's TaskGroup or trio's nursery
124+
done_count = 0
125+
while done_count != len(running_tasks):
126+
done, _ = await tasks.wait(running_tasks)
127+
done_count = len(done)
128+
# If run_one_coro raises an unhandled exception, it's probably a
129+
# programming error, and I want to see it.
130+
if __debug__:
131+
for d in done:
132+
if d.done() and not d.cancelled() and d.exception():
133+
raise d.exception()
134+
return winner_result, winner_index, exceptions
135+
finally:
136+
# Make sure no tasks are left running if we leave this function
137+
for t in running_tasks:
138+
t.cancel()

Lib/test/test_asyncio/test_eager_task_factory.py

-47
Original file line numberDiff line numberDiff line change
@@ -213,53 +213,6 @@ async def run():
213213

214214
self.run_coro(run())
215215

216-
def test_staggered_race_with_eager_tasks(self):
217-
# See https://github.com/python/cpython/issues/124309
218-
219-
async def fail():
220-
await asyncio.sleep(0)
221-
raise ValueError("no good")
222-
223-
async def run():
224-
winner, index, excs = await asyncio.staggered.staggered_race(
225-
[
226-
lambda: asyncio.sleep(2, result="sleep2"),
227-
lambda: asyncio.sleep(1, result="sleep1"),
228-
lambda: fail()
229-
],
230-
delay=0.25
231-
)
232-
self.assertEqual(winner, 'sleep1')
233-
self.assertEqual(index, 1)
234-
self.assertIsNone(excs[index])
235-
self.assertIsInstance(excs[0], asyncio.CancelledError)
236-
self.assertIsInstance(excs[2], ValueError)
237-
238-
self.run_coro(run())
239-
240-
def test_staggered_race_with_eager_tasks_no_delay(self):
241-
# See https://github.com/python/cpython/issues/124309
242-
async def fail():
243-
raise ValueError("no good")
244-
245-
async def run():
246-
winner, index, excs = await asyncio.staggered.staggered_race(
247-
[
248-
lambda: fail(),
249-
lambda: asyncio.sleep(1, result="sleep1"),
250-
lambda: asyncio.sleep(0, result="sleep0"),
251-
],
252-
delay=None
253-
)
254-
self.assertEqual(winner, 'sleep1')
255-
self.assertEqual(index, 1)
256-
self.assertIsNone(excs[index])
257-
self.assertIsInstance(excs[0], ValueError)
258-
self.assertEqual(len(excs), 2)
259-
260-
self.run_coro(run())
261-
262-
263216

264217
class PyEagerTaskFactoryLoopTests(EagerTaskFactoryLoopTests, test_utils.TestCase):
265218
Task = tasks._PyTask

Lib/test/test_asyncio/test_staggered.py

+4-52
Original file line numberDiff line numberDiff line change
@@ -82,64 +82,16 @@ async def test_none_successful(self):
8282
async def coro(index):
8383
raise ValueError(index)
8484

85-
for delay in [None, 0, 0.1, 1]:
86-
with self.subTest(delay=delay):
87-
winner, index, excs = await staggered_race(
88-
[
89-
lambda: coro(0),
90-
lambda: coro(1),
91-
],
92-
delay=delay,
93-
)
94-
95-
self.assertIs(winner, None)
96-
self.assertIs(index, None)
97-
self.assertEqual(len(excs), 2)
98-
self.assertIsInstance(excs[0], ValueError)
99-
self.assertIsInstance(excs[1], ValueError)
100-
101-
async def test_long_delay_early_failure(self):
102-
async def coro(index):
103-
await asyncio.sleep(0) # Dummy coroutine for the 1 case
104-
if index == 0:
105-
await asyncio.sleep(0.1) # Dummy coroutine
106-
raise ValueError(index)
107-
108-
return f'Res: {index}'
109-
11085
winner, index, excs = await staggered_race(
11186
[
11287
lambda: coro(0),
11388
lambda: coro(1),
11489
],
115-
delay=10,
90+
delay=None,
11691
)
11792

118-
self.assertEqual(winner, 'Res: 1')
119-
self.assertEqual(index, 1)
93+
self.assertIs(winner, None)
94+
self.assertIs(index, None)
12095
self.assertEqual(len(excs), 2)
12196
self.assertIsInstance(excs[0], ValueError)
122-
self.assertIsNone(excs[1])
123-
124-
def test_loop_argument(self):
125-
loop = asyncio.new_event_loop()
126-
async def coro():
127-
self.assertEqual(loop, asyncio.get_running_loop())
128-
return 'coro'
129-
130-
async def main():
131-
winner, index, excs = await staggered_race(
132-
[coro],
133-
delay=0.1,
134-
loop=loop
135-
)
136-
137-
self.assertEqual(winner, 'coro')
138-
self.assertEqual(index, 0)
139-
140-
loop.run_until_complete(main())
141-
loop.close()
142-
143-
144-
if __name__ == "__main__":
145-
unittest.main()
97+
self.assertIsInstance(excs[1], ValueError)

Misc/NEWS.d/next/Library/2024-09-23-18-18-23.gh-issue-124309.iFcarA.rst

-1
This file was deleted.

0 commit comments

Comments
 (0)