Skip to content
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

How do I cancel a streaming response on shutdown? #451

Closed
Tracked by #1808
rob-blackbourn opened this issue Oct 16, 2019 · 4 comments · Fixed by #1950
Closed
Tracked by #1808

How do I cancel a streaming response on shutdown? #451

rob-blackbourn opened this issue Oct 16, 2019 · 4 comments · Fixed by #1950

Comments

@rob-blackbourn
Copy link

The code below simulates a server sent event with uvicorn 0.9.

When run, if the HTTP request is not initiated the lifespan startup and shutdown events get called correctly on ^C.

If the HTTP request is made the first ^C will not stop the responses. The second ^C stops the responses, but the shutdown event is not called.

What do I need to do to handle this situation?

import asyncio
from datetime import datetime

import uvicorn

class App:

    def __call__(self, scope):
        if scope['type'] == 'lifespan':
            return self.lifespan
        elif scope['type'] == 'http':
            return self.http

    async def lifespan(self, receive, send):
        """Handle lifespan messages"""

        message = await receive()
        assert message["type"] == "lifespan.startup"
        print('startup')
        await send({"type": "lifespan.startup.complete"})

        message = await receive()
        assert message["type"] == "lifespan.shutdown"
        print('shutdown')
        await send({"type": "lifespan.shutdown.complete"})
        
    async def http(self, receive, send):
        """Handle http messages"""

        message = await receive()
        assert message['type'] == 'http.request'

        while message['more_body']:
            message = await receive()

        await send({
            'type': 'http.response.start',
            'status': 200,
            'headers': [
                (b'cache-control', b'no-cache'),
                (b'content-type', b'text/event-stream'),
                (b'connection', b'keep-alive')
            ]
        })

        while True:
            await send({
                'type': 'http.response.body',
                'body': f'data: {datetime.now().isoformat()}\n\n'.encode('utf-8'),
                'more_body': True
            })

            try:
                # Check the receive, timing out after a second to send more data
                receive_task = asyncio.create_task(receive())
                await asyncio.wait_for(receive_task, 1)
                message = receive_task.result()
                if message['type'] == 'http.disconnect':
                    print('disconnect')
                    break
            except asyncio.TimeoutError:
                print('timeout')
                receive_task.cancel()

if __name__ == '__main__':
    uvicorn.run(App(), port=9009)
@euri10
Copy link
Member

euri10 commented Oct 16, 2019

I didn't try your app, will definitely do as I'm also having issues on shutdown on one of my app and I failed at finding time to make a smaller example than the mess I' currently working on, but this raise interest.

Out of curiosity, are you having the Waiting for background tasks to complete. (CTRL+C to force quit) after the 1st Ctrl+C then it hangs and still performs stuff, then stop at 2nd Ctrl+C or it's behaving differently ?

@rob-blackbourn
Copy link
Author

This is the output I'm getting when I make the http request:

$ python example.py
INFO: Started server process [18979]
INFO: Waiting for application startup.
startup
INFO: Uvicorn running on http://127.0.0.1:9009 (Press CTRL+C to quit)
INFO: ('127.0.0.1', 39418) - "GET / HTTP/1.1" 200
timeout
timeout
^CINFO: Shutting down
INFO: Waiting for connections to close. (CTRL+C to force quit)
timeout
timeout
^CINFO: Finished server process [18979]
$ 

Without making the http request I get:

$ python example.py
INFO: Started server process [20156]
INFO: Waiting for application startup.
startup
INFO: Uvicorn running on http://127.0.0.1:9009 (Press CTRL+C to quit)
^CINFO: Shutting down
INFO: Waiting for application shutdown.
shutdown
INFO: Finished server process [20156]
$ 

@euri10
Copy link
Member

euri10 commented Oct 16, 2019

Ok sorry then so it's a little bit different in your case, you loop indefinitely a few lines

            while self.server_state.connections and not self.force_exit:
                await asyncio.sleep(0.1)

above me and never reach lifespan shutdown as the same consequence.

I'll try to make a simpler reproducible example of my own issue and post it in a separate thread so that I'm not hijacking yours but I feel like there's something similar going on

@Kludex
Copy link
Member

Kludex commented Dec 30, 2022

PR welcome to add a --graceful-timeout. The default value would be None, which means that we don't timeout.

The flag will cancel the background tasks, after the number specified.

The shutdown event should run after the background tasks are cancelled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment