asyncio - add shield scope context manager for improved control flow in sensitive code sections
Dieses Issue hat noch niemand übernommen.
Bewertung
- Schwierigkeit
- 5/5
- Geschätzter Aufwand
- Über eine Woche
- Anfängerfreundlichkeit
- 25/100
Rechercherichtung
Beginne mit der Überprüfung des bestehenden Verhaltens von asyncio.shield und der verknüpften PRs gh-99898 und gh-99899. Die vorgeschlagene Arbeit ist abgeschlossen, wenn ein task-lokaler shield_scope-Kontextmanager den Kontrollfluss durch seinen Block aufrechterhält, die Abbruchanforderung wie beschrieben verzögert und die Abschirmung nicht in untergeordnete Tasks kopiert.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Beschreibung
Feature or enhancement
Add a shield_scope context manager which can shield a block of code from cancellation and block waiting until it completes. A pending cancellation for a task would either be raised during the next non-shielded await or at the end of the task. Shields are task-local and are not be copied into child tasks, to prevent breaking the behavior of child tasks.
await cancellable_work()
with shield_scope():
state = 2
await work_to_complete_after_state_change()
await other_important_work()
print('sync code, cannot be cancelled')
await maybe_cancelled_work()
Pitch
The current shielding mechanism, asyncio.shield works by wrapping one future in another so the cancellation applies to the outer future while the inner future continues to run. While this solution is effective in protecting the awaitable you pass in, control flow becomes muddled (the exception bubbles up while this future is now running detached in the background) and the code the shield covers often has to grow to encompass other resources to avoid control flow issues, limiting the value of the cancellation features of asyncio.
The simplest current workaround to maintaining control flow using asyncio.shield seems to be to wrap up the coroutine in a task and wait on that after the cancellation occurs. This solution adds a lot of overhead (Task object + loop cycle until task can start) to shielded functions, which can add up quickly in hot code paths. If shielded functions are nested, where to re-raise the cancellation also becomes a challenge requiring contextual information to know if we are still inside a coroutine which is shielded.
import asyncio
async def work():
print("task started")
task = asyncio.create_task(asyncio.sleep(5))
try:
await asyncio.shield(task)
except asyncio.CancelledError:
print("task cancellation caught")
await task
print("task ran to completion")
raise
async def main():
task = asyncio.create_task(work())
await asyncio.sleep(1)
task.cancel()
await task
asyncio.run(main())
The following example hopefully provides a more concrete example of how a shield scope could more efficiently solve this shielding / control flow need and be easier to work with.
The following shows how a shield_scope might be used. If the same code were run with asyncio.shield wrapping the publish coroutines, the resource_lock acquired by create_resource would be released before publish completed and the delete_resource coroutine would finish publishing before the create_resource coroutine.
import asyncio
# lets say we have a collection of resources and a pub/sub system
# which we want to send all signals related to those resources to
# lets also say that we want all messages (created, deleted, updated, etc.)
# to be sequential for each resource. to do that, we use a lock for each
# resource. there may be some contention around each lock, as
# each operation for a resource may take awhile, so we want to maintain
# the ability to cancel waiting on the lock at any time (such as a client of a web server might do)
async def publish(msg, delay):
print(f"starting publish: {msg}")
await asyncio.sleep(delay)
print(f"ending publish: {msg}")
async def create_resource(resource_lock):
print("create - wait lock")
await resource_lock.acquire()
print("create - acquire lock")
try:
print("create - resource")
# once we have created the resource, we have to finish publishing
# or the other parts of the system wont know about it
# we have to hold the lock or publishing of the created message
# may interleave with publishing of the deleted message
with shield_scope():
await publish("create - resource", delay=3)
finally:
resource_lock.release()
print("create - release lock")
async def delete_resource(resource_lock):
# the lock is outside the shield so we can still cancel waiting on it
# if there is contention with the lock. maybe we come back later.
print("delete - wait lock")
await resource_lock.acquire()
print("delete - acquire lock")
try:
print("delete - resource")
with shield_scope():
await publish("delete - resource", delay=2)
finally:
resource_lock.release()
print("delete - release lock")
async def main():
resource_lock = asyncio.Lock()
creator = asyncio.create_task(create_resource(resource_lock))
await asyncio.sleep(0)
deleter = asyncio.create_task(delete_resource(resource_lock))
await asyncio.sleep(0)
creator.cancel()
await asyncio.sleep(5)
asyncio.run(main())
Previous discussion
- trio - CancelScope.shield
- curio - disable_cancellation discussion
- curio - disable_cancellation docs
- asyncio - mentioned in TaskGroup discussion
Linked PRs
- gh-99898
- gh-99899
- Vorherrschende Sprache
- Python
- Sterne
- 77.2k
- Forks
- 36k
- Ø Merge
- 1 T. 9 Std.
- Gemergte PRs (30 T.)
- 558
Beitragsleitfaden
Erste Schritte
- Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
- Forken Sie das Repository und arbeiten Sie in einem Branch.
- Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.
Mehr aus python/cpython
-
docs pending
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 78/100
-
stdlib type-feature
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 78/100
-
stdlib type-feature
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 72/100
-
build type-bug
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 76/100
-
stdlib topic-email type-feature
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 70/100
Ähnliche Issues
-
bug
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 86/100
zostera/django-bootstrap4#894 ·
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 78/100
use-agent-os/agent-os#3276 ·
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 88/100
zephyrproject-rtos/zephyr#119726 ·
-
area/auth bug comp/agent P3 platform/discord type/security
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 88/100
NousResearch/hermes-agent#117848 ·
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 82/100
zilliztech/memsearch#759 ·