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

Django 5.1, ASGI, SSE #1501

Open
mamashin opened this issue Dec 1, 2024 · 6 comments
Open

Django 5.1, ASGI, SSE #1501

mamashin opened this issue Dec 1, 2024 · 6 comments

Comments

@mamashin
Copy link

mamashin commented Dec 1, 2024

Hello.

Sorry, I have a question about Django and ASGI. I saw that a similar topic has already been discussed, but I couldn't find a solution for myself.

Unit 1.33
Django 5.1 (support async handling disconnects)
Python 3.11.2
Debian 12

Test Django code:

def sse(request: HttpRequest) -> HttpResponseBase:
    """Small demo of the basic idea of SSE without any redis or other complexity"""
    async def stream(request: HttpRequest) -> AsyncGenerator[str, None]:
        try:
            counter = 0
            while True:
                counter += 1
                if counter != 1:
                    await asyncio.sleep(1.0)
                yield f"data: <div>{counter}</div>\n\n"
        except asyncio.CancelledError:
            logger.debug("SSE Client disconnected")
            raise

    return StreamingHttpResponse(
        streaming_content=stream(request),
        content_type="text/event-stream"
    )

When I run this code under the Uvicorn ASGI server, after closing the connection, I see "SSE Client disconnected" in my dubug log. However, under Unit, nothing happens, with only in unit log:

2024/12/01 11:29:31 [info] 40085#40121 *11 writev(44, 2) failed (32: Broken pipe)

It seems that Django does not detect that the connection is closed, and this is only apparent when I restart Unit. In the logs:

2024/12/01 11:02:55 [warn] 34253#34253 [unit] #516: active request on ctx quit
2024/12/01 11:02:55 [warn] 34253#34253 [unit] #404: active request on ctx free

In a production environment when I use Redis, I see many connections with Unit and Redis in the ESTABLISHED state, even though many connections are already closed. This is a problem for me.

What am I doing wrong 🤔 ?

@mamashin
Copy link
Author

mamashin commented Dec 1, 2024

small addon.

this is my debug ASGI entry point:

import os
from loguru import logger
from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'conf.settings')
django_application = get_asgi_application()

async def application(scope, receive, send):
    if scope['type'] == 'http':
        # Let Django handle only ASGI HTTP requests
        async def receive_wrapper():
            message = await receive()
            logger.debug(f'ASGI application receive message: {message}')
            await logger.complete()
            return message
        await django_application(scope, receive_wrapper, send)
    else:
        pass

When I connected, I see:

ASGI application receive message: {'type': 'http.request'}

But when I disconnected - log is empty.

As I said, if I run this test under Uvicorn, I see:

ASGI application receive message: {'type': 'http.disconnect'}

Where I lost 'http.disconnect' while working with Unit ?

@gourav-kandoria
Copy link
Contributor

@ac000 I have opened a draft pr for this issue. Not sure, it is right approach or not but have tried to come up with a possible solution. Could you please review it once. Have added more details as description and commit msg
https://github.com/nginx/unit/pull/1556/files

@ac000
Copy link
Member

ac000 commented Feb 7, 2025

@mamashin

Do you have a simple purely ASGI reproducer (no frameworks involved) and steps to reproduce?

Nevermind, I have one from @gourav-kandoria

@ac000
Copy link
Member

ac000 commented Feb 7, 2025

I.e. something that simply shows the Python app is not getting notified when clients close the socket?

@ac000
Copy link
Member

ac000 commented Feb 8, 2025

Hmm, I'm seeing something slightly different...

With

async def application(scope, receive, send):                                    
    while True:                                                                 
        m = await receive()                                                     
        if m['type'] == 'http.disconnect':                                      
                print("Client Disconnect")                                      
                break                                                           
                                                                                
        await send(                                                             
            {                                                                   
                "type": "http.response.start",                                                                  
                "status": 200,                                                  
                "headers": [[b"content-type", b"text/plain"]],                  
            }                                                                   
        )                                                                       
        await send({"type": "http.response.body", "body": b"Hello, world!"})

I get the "Client Disconnect" message after every request, even though Firefox is using keep-alive.

Indeed, unlike with WebSockets where Firefox will close the connection if you close the browser tab, Firefox seems to keep the socket open even if you close the tab, if you restore the tab, it continues using the same socket.

Not sure why we're sending a http.disconnect after request though...

@ac000
Copy link
Member

ac000 commented Feb 8, 2025

Seems to be something peculiar to the way Server-Sent Events are handled...

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

No branches or pull requests

3 participants