Skip to content

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

Merged
dpgeorge merged 1 commit into
micropython:masterfrom
jimmo:asyncio-server-close
Oct 2, 2023
Merged

uasyncio/stream.py: Handle cancellation before server start.#8895
dpgeorge merged 1 commit into
micropython:masterfrom
jimmo:asyncio-server-close

Conversation

@jimmo

@jimmo jimmo commented Jul 11, 2022

Copy link
Copy Markdown
Member

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

codecov-commenter commented Jul 11, 2022

Copy link
Copy Markdown

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
Copy Markdown
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

jimmo commented Jul 12, 2022

Copy link
Copy Markdown
Member Author

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
Copy Markdown
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

jimmo commented Jul 12, 2022

Copy link
Copy Markdown
Member Author

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
Copy Markdown
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

jimmo commented Jul 12, 2022

Copy link
Copy Markdown
Member Author

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

Done

Comment thread extmod/uasyncio/stream.py Outdated

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Comment thread extmod/uasyncio/stream.py Outdated
Comment thread tests/net_hosted/uasyncio_start_server.py Outdated
@jimmo
jimmo force-pushed the asyncio-server-close branch 4 times, most recently from 53c6ba7 to fde0191 Compare July 19, 2022 13:47
@jimmo

jimmo commented Jul 19, 2022

Copy link
Copy Markdown
Member Author

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

github-actions Bot commented Oct 2, 2023

Copy link
Copy Markdown

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

dpgeorge commented Oct 2, 2023

Copy link
Copy Markdown
Member

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
Copy Markdown

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