Skip to content

Commit 1d7ece3

Browse files
committed
make broken_by attribute a list, clean up tests
1 parent b81e297 commit 1d7ece3

File tree

3 files changed

+82
-64
lines changed

3 files changed

+82
-64
lines changed

src/trio/_core/_parking_lot.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@
7373

7474
import inspect
7575
import math
76-
import warnings
7776
from collections import OrderedDict
7877
from typing import TYPE_CHECKING
7978

@@ -152,7 +151,7 @@ class ParkingLot:
152151
# {task: None}, we just want a deque where we can quickly delete random
153152
# items
154153
_parked: OrderedDict[Task, None] = attrs.field(factory=OrderedDict, init=False)
155-
broken_by: Task | None = None
154+
broken_by: list[Task] = attrs.field(factory=list, init=False)
156155

157156
def __len__(self) -> int:
158157
"""Returns the number of parked tasks."""
@@ -176,7 +175,7 @@ async def park(self) -> None:
176175
breaks before we get to unpark.
177176
178177
"""
179-
if self.broken_by is not None:
178+
if self.broken_by:
180179
raise _core.BrokenResourceError(
181180
f"Attempted to park in parking lot broken by {self.broken_by}",
182181
)
@@ -289,16 +288,13 @@ def break_lot(self, task: Task | None = None) -> None:
289288
"""
290289
if task is None:
291290
task = _core.current_task()
292-
if self.broken_by is not None:
293-
if self.broken_by != task:
294-
warnings.warn(
295-
RuntimeWarning(
296-
f"{task} attempted to break parking lot {self} already broken by {self.broken_by}",
297-
),
298-
stacklevel=2,
299-
)
291+
292+
# if lot is already broken, just mark this as another breaker and return
293+
if self.broken_by:
294+
self.broken_by.append(task)
300295
return
301-
self.broken_by = task
296+
297+
self.broken_by.append(task)
302298

303299
for parked_task in self._parked:
304300
_core.reschedule(

src/trio/_core/_run.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1900,8 +1900,7 @@ def task_exited(self, task: Task, outcome: Outcome[Any]) -> None:
19001900
# break parking lots associated with the task exiting
19011901
if task in GLOBAL_PARKING_LOT_BREAKER:
19021902
for lot in GLOBAL_PARKING_LOT_BREAKER[task]:
1903-
if lot.broken_by is None:
1904-
lot.break_lot(task)
1903+
lot.break_lot(task)
19051904
del GLOBAL_PARKING_LOT_BREAKER[task]
19061905

19071906
if (

src/trio/_core/_tests/test_parking_lot.py

Lines changed: 73 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import re
34
from typing import TypeVar
45

56
import pytest
@@ -233,6 +234,53 @@ async def dummy_task(
233234

234235

235236
async def test_parking_lot_breaker_basic() -> None:
237+
"""Test basic functionality for breaking lots."""
238+
lot = ParkingLot()
239+
task = current_task()
240+
241+
# defaults to current task
242+
lot.break_lot()
243+
assert lot.broken_by == [task]
244+
245+
# breaking the lot again with the same task appends another copy in `broken_by`
246+
lot.break_lot()
247+
assert lot.broken_by == [task, task]
248+
249+
# trying to park in broken lot errors
250+
broken_by_str = re.escape(str([task, task]))
251+
with pytest.raises(
252+
_core.BrokenResourceError,
253+
match=f"^Attempted to park in parking lot broken by {broken_by_str}$",
254+
):
255+
await lot.park()
256+
257+
258+
async def test_parking_lot_break_parking_tasks() -> None:
259+
"""Checks that tasks currently waiting to park raise an error when the breaker exits."""
260+
261+
async def bad_parker(lot: ParkingLot, scope: _core.CancelScope) -> None:
262+
add_parking_lot_breaker(current_task(), lot)
263+
with scope:
264+
await trio.sleep_forever()
265+
266+
lot = ParkingLot()
267+
cs = _core.CancelScope()
268+
269+
# check that parked task errors
270+
with RaisesGroup(
271+
Matcher(_core.BrokenResourceError, match="^Parking lot broken by"),
272+
):
273+
async with _core.open_nursery() as nursery:
274+
nursery.start_soon(bad_parker, lot, cs)
275+
await wait_all_tasks_blocked()
276+
277+
nursery.start_soon(lot.park)
278+
await wait_all_tasks_blocked()
279+
280+
cs.cancel()
281+
282+
283+
async def test_parking_lot_breaker_registration() -> None:
236284
lot = ParkingLot()
237285
task = current_task()
238286

@@ -254,58 +302,60 @@ async def test_parking_lot_breaker_basic() -> None:
254302
):
255303
remove_parking_lot_breaker(task, lot)
256304

257-
# defaults to current task
258-
lot.break_lot()
259-
assert lot.broken_by == task
260-
261-
# breaking the lot again with the same task is a no-op
305+
# registering a task as breaker on an already broken lot is fine
262306
lot.break_lot()
263-
264-
# registering a task as a breaker on an already broken lot is a no-op.
265307
child_task = None
266308
async with trio.open_nursery() as nursery:
267309
child_task = await nursery.start(dummy_task)
268310
add_parking_lot_breaker(child_task, lot)
269311
nursery.cancel_scope.cancel()
312+
assert lot.broken_by == [task, child_task]
270313

271314
# manually breaking a lot with an already exited task is fine
272315
lot = ParkingLot()
273316
lot.break_lot(child_task)
317+
assert lot.broken_by == [child_task]
274318

275319

276-
async def test_parking_lot_breaker_warnings() -> None:
320+
async def test_parking_lot_breaker_rebreak() -> None:
277321
lot = ParkingLot()
278322
task = current_task()
279323
lot.break_lot()
280324

281-
warn_str = "attempted to break parking .* already broken by .*"
282-
# breaking an already broken lot with a different task gives a warning
325+
# breaking an already broken lot with a different task is allowed
283326
# The nursery is only to create a task we can pass to lot.break_lot
284327
async with trio.open_nursery() as nursery:
285328
child_task = await nursery.start(dummy_task)
286-
with pytest.warns(
287-
RuntimeWarning,
288-
match=warn_str,
289-
):
290-
lot.break_lot(child_task)
329+
lot.break_lot(child_task)
291330
nursery.cancel_scope.cancel()
292331

293-
# and doesn't change broken_by
294-
assert lot.broken_by == task
332+
# and appends the task
333+
assert lot.broken_by == [task, child_task]
334+
295335

336+
async def test_parking_lot_multiple_breakers_exit() -> None:
296337
# register multiple tasks as lot breakers, then have them all exit
297338
# No warning is given on task exit, even if the lot is already broken.
298339
lot = ParkingLot()
299-
child_task = None
300340
async with trio.open_nursery() as nursery:
301-
child_task = await nursery.start(dummy_task)
341+
child_task1 = await nursery.start(dummy_task)
302342
child_task2 = await nursery.start(dummy_task)
303343
child_task3 = await nursery.start(dummy_task)
304-
add_parking_lot_breaker(child_task, lot)
344+
add_parking_lot_breaker(child_task1, lot)
305345
add_parking_lot_breaker(child_task2, lot)
306346
add_parking_lot_breaker(child_task3, lot)
307347
nursery.cancel_scope.cancel()
308348

349+
# I think the order is guaranteed currently, but doesn't hurt to be safe.
350+
assert set(lot.broken_by) == {child_task1, child_task2, child_task3}
351+
352+
353+
async def test_parking_lot_breaker_register_exited_task() -> None:
354+
lot = ParkingLot()
355+
child_task = None
356+
async with trio.open_nursery() as nursery:
357+
child_task = await nursery.start(dummy_task)
358+
nursery.cancel_scope.cancel()
309359
# trying to register an exited task as lot breaker errors
310360
with pytest.raises(
311361
trio.BrokenResourceError,
@@ -314,34 +364,7 @@ async def test_parking_lot_breaker_warnings() -> None:
314364
add_parking_lot_breaker(child_task, lot)
315365

316366

317-
async def test_parking_lot_breaker_bad_parker() -> None:
318-
async def bad_parker(lot: ParkingLot, scope: _core.CancelScope) -> None:
319-
add_parking_lot_breaker(current_task(), lot)
320-
with scope:
321-
await trio.sleep_forever()
322-
323-
lot = ParkingLot()
324-
cs = _core.CancelScope()
325-
326-
# check that parked task errors
327-
with RaisesGroup(
328-
Matcher(_core.BrokenResourceError, match="^Parking lot broken by"),
329-
):
330-
async with _core.open_nursery() as nursery:
331-
nursery.start_soon(bad_parker, lot, cs)
332-
await wait_all_tasks_blocked()
333-
334-
nursery.start_soon(lot.park)
335-
await wait_all_tasks_blocked()
336-
337-
cs.cancel()
338-
339-
# check that trying to park in broken lot errors
340-
with pytest.raises(_core.BrokenResourceError):
341-
await lot.park()
342-
343-
344-
async def test_parking_lot_weird() -> None:
367+
async def test_parking_lot_break_itself() -> None:
345368
"""Break a parking lot, where the breakee is parked.
346369
Doing this is weird, but should probably be supported.
347370
"""
@@ -359,5 +382,5 @@ async def return_me_and_park(
359382
Matcher(_core.BrokenResourceError, match="^Parking lot broken by"),
360383
):
361384
async with _core.open_nursery() as nursery:
362-
task = await nursery.start(return_me_and_park, lot)
363-
lot.break_lot(task)
385+
child_task = await nursery.start(return_me_and_park, lot)
386+
lot.break_lot(child_task)

0 commit comments

Comments
 (0)