-
Notifications
You must be signed in to change notification settings - Fork 2
Prep 2.0.0 #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Prep 2.0.0 #27
Conversation
|
@JaagupAverin can you review this? |
|
Tests passing, works as a drop-in-replacement for my local project as well. |
|
It is not enough to use |
|
The most important place to add The second place is "if the lock is reentrant", which handles the case of reacquiring the lock. Whether a checkpoint should be added here is debatable. The effect on performance will be purely negative (checkpoints are not about performance at all), but it may help in some complex cases. In my library, which also provides reentrant locks (but thread-safe, see x42005e1f/aiologic#5 for performance comparisons), there is no checkpoint in the second case. But I think I will change this behavior for the next version. |
|
To be clear, this is a best practices issue and not a logical error you're speaking of (although of course as a library this should conform to best practices anyways)? |
|
There may be expectations from a Trio-compatible library that it will also inherit its semantics. AnyIO adheres to the same semantics. I think this is not a best practices issue, but a compatibility issue. And no, the asyncio ecosystem has a different model, so this practice does not need to be followed there. |
|
For example, if you replace |
|
Im thinking from the aspect of where best put these checkpoints in this code. It could be in the anyio specific code but from what I understand it's a good practice to have these checks in any async code anyways. |
|
I think you could add an additional method to A combination of |
|
Thank you both @JaagupAverin and @x42005e1f for your reviews and discussion. Let's take a staged approach. For 2.0.0 I will make the refactorisation into base class so that users can use AnyIO implementation or asyncio implementation. To integrate @x42005e1f 's points I'd like to begin by finding a good regression test. If we can pinpoint the divergence of behaviour via a test then the checkpointing will make sense to add.
|
|
I'm also happy to hold-off on merging until we've sorted the above. |
|
I think the easiest and and most dangerous behaviour to test is the endless co-operative while-loop: async def test_scheduling() -> None:
lock = AnyIOFairAsyncRLock()
async def while_loop() -> None:
while True:
await lock.acquire()
#await anyio.sleep(0)
async with anyio.create_task_group() as tg:
tg.start_soon(while_loop)
await anyio.lowlevel.checkpoint()
tg.cancel_scope.cancel()Applies to asyncio as well: @pytest.mark.asyncio
async def test_scheduling():
lock = FairAsyncRLock()
async def while_loop():
while True:
await lock.acquire()
#await asyncio.sleep(0)
t = asyncio.create_task(while_loop())
await asyncio.sleep(0) # Give the inner task a chance to run
t.cancel()
await tBoth of these loops will spin forever since the cancellation is never checked. Adding a sleep of course fixes the issue. I don't personally have any feelings on which level this check should be enforced. @x42005e1f appears to be much more knowledgeable on async code so I'd go with their recommendations. |
# Conflicts: # src/fair_async_rlock/tests/test_anyio_fair_async_rlock.py # src/fair_async_rlock/tests/test_fair_async_rlock.py
* add regression test for nested reentrant (still fails)
It actually does not matter so much where the cancellation checking is done and where the switching to other tasks takes place. "At the start and at the end" is what it usually looks like. What matters is that both of these actions are unconditional: a successful function call should always do both the check and the switch.
import anyio
import pytest
from fair_async_rlock import AnyIOFairAsyncRLock
@pytest.mark.anyio
async def test_anyio_checkpoints():
lock = AnyIOFairAsyncRLock()
async def acquirer():
async with lock:
pass
async def neighbor():
if not lock.locked():
await anyio.sleep(0)
assert lock.locked()
# check for scheduling
async with anyio.create_task_group() as tg:
tg.start_soon(acquirer)
tg.start_soon(neighbor)
# check for cancellation
with anyio.move_on_after(0):
with pytest.raises(anyio.get_cancelled_exc_class()):
await acquirer() |
|
I have added 7 tests:
Both nested reentrant path's are failing. Getting @x42005e1f note, for reentrant path upon cancellation # If the lock is reentrant, acquire it immediately
if self.is_owner(task=me):
self._count += 1
try:
await self._checkpoint()
except self._get_cancelled_exc_class():
# Cancelled, while reentrant, so release the lock
self._owner_transfer = False
self._owner = me
self._count = 1
self._current_task_release()
raise
returnBut, this feel clumsy. There must be a better way. Note, I do a similar thing at line 95, when waiter event isn't found in the queue. |
|
Note that in this example: ... # 0
async with lock:
... # 1
async with lock:
... # 2
async with lock:
... # 3, cancelled!You should get a sequence of |
It is much more prosaic than that. An asynchronous function is a coroutine factory. A coroutine is a slightly extended generator. example.pyimport asyncio
class Checkpoint:
def __await__(self):
print("before")
yield None # asyncio-specific!
print("after")
class NotCheckpoint:
def __await__(self):
print("before")
yield from () # no yield
print("after")
async def printer(string):
print(string)
async def test():
asyncio.create_task(printer("from task 1"))
await Checkpoint()
# before
# from task 1
# after
asyncio.create_task(printer("from task 2"))
await NotCheckpoint()
# before
# after
await asyncio.sleep(0)
# from task 2
print(list(Checkpoint().__await__())) # [None]
print(list(NotCheckpoint().__await__())) # []
print(list(asyncio.sleep(0).__await__())) # [None]
future = asyncio.get_running_loop().create_future()
future_it = future.__await__()
print(next(future_it)) # <Future pending> -- yield future
future.set_result("result")
try:
next(future_it)
except StopIteration as exc:
print(exc.value) # result
print(await future) # result
asyncio.run(test()) |
@x42005e1f this property is already met in this test (which I just added). But, this one is failing. |
|
No, that is not what I meant. import asyncio
import pytest
from fair_async_rlock import FairAsyncRLock
@pytest.mark.asyncio
async def test_chained_lock_count_reentrant():
lock = FairAsyncRLock()
async def b():
assert lock._count == 1
assert lock._owner is not None
asyncio.current_task().cancel()
with pytest.raises(asyncio.CancelledError):
await lock.acquire()
assert lock._count == 1
assert lock._owner is not None
async def a():
assert lock._count == 0
assert lock._owner is None
async with lock:
await b()
assert lock._count == 0
assert lock._owner is None
await a()It fails with |
|
@x42005e1f I'm not getting the same error for your |
Integates #22 by allowing user to choose asyncio consistent behaviour (of task cancellation) or Trio compatible.
Two implementations: