channels icon indicating copy to clipboard operation
channels copied to clipboard

Document long polling

Open Christophe31 opened this issue 7 years ago • 10 comments

It looks like channel handle long polling via ResponseLater exception but no test or documentation on how to retreive connexion to send data back and close it.

I supose we may use channels.handler.ViewConsumer the same way we would use a WebsocketConsumer, but I didn't find this info in any doc.

Christophe31 avatar Feb 10 '17 16:02 Christophe31

I'm testing ResponseLater for one of my views but can't make it work. I have some middleware, and it seems that any middleware will always force a HttpResponse. The ResponseLater exception won't reach the Channels code (in AsgiHandler) ignoring this specific Exception, it will get a HttpResponse object with HTML content describing the error.

The AsgiHandler wraps BaseHandler.get_response() in a try/except and then ignores the ResponseLater exception. But if my view raises the ResponseLater, it is converted to a HttpResponse before it reaches the AsgiHandler. I'm still using MIDDLEWARE_CLASSES, but I'm not sure if this is the reason.

The channels/handler.AsgiHandler relies uses BaseHandler.load_middleware(), which wraps a convert_exception_to_response around all middleware and around get_response. If this is not somehow monkeypatched the exception won't be bubbled up.

In channels < 0.8 the ResponseLater exception thrown by a Django View was supported through monkeypatching in channels/hacks.py . This was removed in commit #b9464ca149a4f66ebeb8801254cdd6d1b12cd58d . Restoring it doesn't help me either.

Is ResponseLater still supported?

ivorbosloper avatar Dec 06 '17 09:12 ivorbosloper

Another design could be to annotate the Response object with an attribute which signifies the ResponseLater intent. Like:

def my_view(request):
    response = HttpResponse("", status=200)
    response._channels_response_later = True
    return response

The channels/handler.AsgiHandler should then check for this attribute. This will work as long as the middleware does not replace the response object, but at least it's a working option.

ivorbosloper avatar Dec 06 '17 16:12 ivorbosloper

Yes, ResponseLater does not really interact well with any middleware. It's been replaced in Channels 2 by async HTTP views instead, so you can await, but of course you give up old-style middleware entirely (channels 2 allows for ASGI middleware, which I need to document, and which works quite like current Django middleware but allows for async stuff)

andrewgodwin avatar Dec 06 '17 19:12 andrewgodwin

@andrewgodwin Thanks for your reply. I suspect Channels 2 async requires python3. I would love to have some mechanism supported with middleware to skip responses, at least until we all can move to Channels 2. I think status code 100 (Continue) or some custom header would be more suitable for decorating the response. I've created pull-request #803

Some background; some of my Django views use django database/session data to setup a http-request to a background service (http://example.com/user={request.user.username}), block and wait only to return the result-content when available. This blocks my workers (which somehow end up using a lot of memory). I'm trying to offload these http-proxy requests to an asyncio worker, which posts the result in the http-response reply-channel. I'll try to share this at https://github.com/ivorbosloper/channels_async so there's an example for using this feature.

ivorbosloper avatar Dec 07 '17 21:12 ivorbosloper

Hi @ivorbosloper,

Probably you choose wrong place to hold a request. You shouldn't block channels worker with any kind of waiting process.

Maybe my example can give you some inspiration. Here https://bitbucket.org/proofit404/algont I setup MJPEG stream which stays on Daphne and worker handler lifetime is about few milliseconds to send one frame to that stream.

proofit404 avatar Dec 08 '17 07:12 proofit404

Thanks @proofit404 , I hadn't found a nice example of how to plug in on interface-worker level before. The point for this fix is to allow me to use existing Django view logic (middleware, orm queries, stuff all Django developers work with) to define a task, and then handle this task asynchronously in an asyncio loop for scaling (celery would fit but doesn't run in an asyncio loop) and then send the result in the http-reply channel.

By plugging in on Django-view level, existing code is reusable and the impact for developers (the API) is minimal/very simple. If you plugin at Channel Message level, you have to (re)write logic to get a session/do database requests, and send some custom response, all in Channel/message semantics.

ivorbosloper avatar Dec 08 '17 11:12 ivorbosloper

My main concern about solution suggested by you, that you're trying to store state internally in the worker process. While Channels 1.0 is a synchronous solution all the way down to the message handler in the worker instance.

I think something different can feet:

For example, we have three points in code:

@channel_session
def handle_long_polling_request(message):
    try:
        request = apply_django_middlewares(make_asgi_request(message))
    except Unathorized:
        message.reply_channel.send({'status_code': 401})
        return
    clone_session(request.session, to=channel_session)
    channel_session['user_id'] = request.user.pk
    store_reply_channel_someway() # for example include it in the Group
    # Do not send anithing to user, request will be hold by Daphne, worker will process next message.

@channel_session
def send_polling_response(message):
    Channel(stored_reply_channel).send(compose_long_polling_responce(message))
    # Daphne will reply to the waiting client, worker will process next message. No blocking at all.

# In the other system part we detect that interesting event
# happened and send it in a message to the long polling
# response handler.

proofit404 avatar Dec 08 '17 12:12 proofit404

I'm going to leave this open as we still need a good example for channels 2, but it will be easier to do now you can write it as an async consumer (might be good to get #841 done first)

andrewgodwin avatar Feb 02 '18 07:02 andrewgodwin

I notice that we don't yet have an async WebsocketConsumer or an async JsonWebsocketConsumer. Would be benificial for applications that need to do a longpoll in response to a websocket noticification.

brianmay avatar Feb 04 '18 23:02 brianmay

I also noticed that a few hours ago and so I committed them in: https://github.com/django/channels/commit/de82aaa660e85af6d1945b445bb6ade4896e2f50

They'll be in the next release :)

andrewgodwin avatar Feb 04 '18 23:02 andrewgodwin