Skip to content

uasyncio/stream.py: Handle cancellation before server start. #8895

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

Merged
merged 1 commit into from
Oct 2, 2023

Conversation

jimmo
Copy link
Member

@jimmo jimmo commented Jul 11, 2022

Originally reported here: https://forum.micropython.org/viewtopic.php?f=20&t=12678&p=68845#p68791

The following code:

  server = await asyncio.start_server(...)
  async with server:
    ... code that raises ...

would lose the original exception because the server's task would not have had a chance to be scheduled yet, and so awaiting the task in wait_closed would raise the cancellation instead of the original exception.

In this case the code that raised was calling server.serve_forever() which we don't support.

@codecov-commenter
Copy link

codecov-commenter commented Jul 11, 2022

Codecov Report

Merging #8895 (977dc9a) into master (a93ebd0) will not change coverage.
The diff coverage is n/a.

@@           Coverage Diff           @@
##           master    #8895   +/-   ##
=======================================
  Coverage   98.39%   98.39%           
=======================================
  Files         158      158           
  Lines       20939    20939           
=======================================
  Hits        20602    20602           
  Misses        337      337           

@dpgeorge dpgeorge added the extmod Relates to extmod/ directory in source label Jul 12, 2022
@dpgeorge
Copy link
Member

Would it be better/simpler to do await asyncio.sleep(0) at the end of start_server(), just before returning the srv object?

@jimmo jimmo closed this Jul 12, 2022
@jimmo jimmo reopened this Jul 12, 2022
@jimmo
Copy link
Member Author

jimmo commented Jul 12, 2022

I'm not sure... is the fact that the scheduler uses a fifo queue and therefore you know all other ready tasks will execute once during a sleep(0) just an implementation detail or something that can be relied upon?

@dpgeorge
Copy link
Member

is the fact that the scheduler uses a fifo queue and therefore you know all other ready tasks will execute once during a sleep(0) just an implementation detail or something that can be relied upon?

The scheduler is guaranteed to be fair, so a await sleep(0) (actually any await) will not return until all other previously-scheduled tasks (those that are immediately runnable) have been run.

So then it's just a question as to whether asyncio.create_task(t) guarantees that t is made immediately runnable, or if there needs to be more than one scheduling round to run the first part of t. In practice it is made immediately runnable, but I'm not sure we can guarantee that forever.

@jimmo
Copy link
Member Author

jimmo commented Jul 12, 2022

So then it's just a question as to whether asyncio.create_task(t) guarantees that t is made immediately runnable, or if there needs to be more than one scheduling round to run the first part of t. In practice it is made immediately runnable, but I'm not sure we can guarantee that forever.

I guess this is part of the asyncio core... so if we do change that behavior then we can fix it. I don't mind either way, let me know what you want me to do.

@dpgeorge
Copy link
Member

Let's leave it as you have it. It's the most efficient way (adding an await will add a delay).

Can you please add a test? Maybe in tests/net_hosted/uasyncio_start_server.py

@jimmo jimmo force-pushed the asyncio-server-close branch from cdc1a26 to 704a0e9 Compare July 12, 2022 12:42
@jimmo
Copy link
Member Author

jimmo commented Jul 12, 2022

Can you please add a test? Maybe in tests/net_hosted/uasyncio_start_server.py

Done

await self.task
try:
await self.task
except core.CancelledError:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this now unintentionally suppress cancellations of tasks that wait on the server? Eg:

async def task():
    srv = asyncio.start_server(...)
    srv.close()
    await srv.wait_closed()

async def main():
    t = asyncio.create_task(task())
    asyncio.sleep(0)
    t.cancel() # CancelledError will be suppressed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you're right.

If the cancellation is due to Server.close() then the CancelledError should be suppressed. I have pushed a version that makes this work, but it's much more code.

Perhaps we should go with the sleep() version instead (which doesn't have this issue)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the sleep version is the way to go. I've pushed that and updated the tests with a case that was broken with the initial version of this PR based on your suggestion above.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good.

(There may be some internal state in Task that allows us to determine if the task is scheduled for the first time or not, but I think using that (if it exists) would make it too complex.)

@jimmo jimmo force-pushed the asyncio-server-close branch 2 times, most recently from b2442f0 to 534dbd5 Compare July 12, 2022 14:15
@jimmo jimmo force-pushed the asyncio-server-close branch 4 times, most recently from 53c6ba7 to fde0191 Compare July 19, 2022 13:47
@jimmo
Copy link
Member Author

jimmo commented Jul 19, 2022

Updated.

This turned out to be a bit more complicated to deal with cancellation (as usual!) plus a detour to #8929, but it now matches CPython on net_inet/uasyncio_start_server.py and correctly closes the listening socket in all cases.

@dpgeorge dpgeorge force-pushed the asyncio-server-close branch from fde0191 to d42fed5 Compare October 2, 2023 02:23
@github-actions
Copy link

github-actions bot commented Oct 2, 2023

Code size report:

   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:  +128 +0.016% standard
      stm32:   +72 +0.018% PYBV10
     mimxrt:   +72 +0.020% TEENSY40
        rp2:   +72 +0.022% RPI_PICO
       samd:   +72 +0.028% ADAFRUIT_ITSYBITSY_M4_EXPRESS

The following code:

  server = await asyncio.start_server(...)
  async with server:
    ... code that raises ...

would lose the original exception because the server's task would not have
had a chance to be scheduled yet, and so awaiting the task in wait_closed
would raise the cancellation instead of the original exception.

Additionally, ensures that explicitly cancelling the parent task delivers
the cancellation correctly (previously was masked by the server loop), now
this only happens if the server was closed, not when the task was
cancelled.

Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
@dpgeorge
Copy link
Member

dpgeorge commented Oct 2, 2023

This was +80 bytes on stm32... that's a lot... I renamed the self._closed member to self.state to save a qstr and it reduced that down to +72 bytes.

The main increase in code size comes from the except; see #4398.

@dpgeorge dpgeorge merged commit 977dc9a into micropython:master Oct 2, 2023
RetiredWizard pushed a commit to RetiredWizard/micropython that referenced this pull request Feb 12, 2024
Fix a couple of incorrect type annotations
RetiredWizard added a commit to RetiredWizard/micropython that referenced this pull request Feb 13, 2024
@RetiredWizard
Copy link

sorry about that, ignore my references here 😦

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
extmod Relates to extmod/ directory in source
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants