channels icon indicating copy to clipboard operation
channels copied to clipboard

Docs: Confusing example around background workers

Open mattbasta opened this issue 4 years ago • 5 comments

I was looking at the docs for background workers:

https://channels.readthedocs.io/en/latest/topics/worker.html

These docs show a very useful example of how to configure the router for two worker channels:

application = ProtocolTypeRouter({
    ...
    "channel": ChannelNameRouter({
        "thumbnails-generate": consumers.GenerateConsumer,
        "thumbnails-delete": consumers.DeleteConsumer,
    }),
})

These channels refer to the channel_layer.send command in the previous section of the doc.

However, the next paragraph deviates to a hypothetical example:

You’ll be specifying the type values of the individual events yourself when you send them, so decide what your names are going to be and write consumers to match. For example, here’s a basic consumer that expects to receive an event with type test.print, and a text value containing the text to print:

followed by a snippet for a PrintConsumer class. While this is not un-useful, it's very confusing because it leaves out some important details. I'm left asking how the type maps to the method name? Why did the . become _? How would the - in the thumbnail example be translated to a method name? Perhaps instead of the PrintConsumer example, a very simple stubbed-out implementation of the GenerateConsumer or DeleteConsumer could be written instead.

It's also unclear when I'd want to have multiple methods on a single consumer (using different types) versus having multiple consumers that handle fewer types each.

Last, the ChannelNameRouter example on this page seems to conflict with the guidance in the Routing docs: https://channels.readthedocs.io/en/latest/topics/routing.html#channelnamerouter

Thanks in advance for you time and attention!

mattbasta avatar Apr 26 '20 01:04 mattbasta

The . in the event type is transformed to a _ in the method name in channels.consumer.get_handler_name:

def get_handler_name(message):
    """
    Looks at a message, checks it has a sensible type, and returns the
    handler name for that type.
    """
    # Check message looks OK
    if "type" not in message:
        raise ValueError("Incoming message has no 'type' attribute")
    if message["type"].startswith("_"):
        raise ValueError("Malformed type in message (leading underscore)")
    # Extract type and replace . with _
    return message["type"].replace(".", "_")

The '-' in the thumbnail example doesn't translate to a method name at all. 'thumbnails-generate' and 'thumbnails-delete' are group names, so you would send a message to that group and the worker will pick it up and handle it, by executing a method based on the event type.

daleevans avatar Jun 15 '20 17:06 daleevans

Please assign me this task.

aliya-rahmani avatar Sep 26 '20 06:09 aliya-rahmani

I also struggled with the type-method_name pair.

I followed the tutorial and find the brief explanation of the _ (underscore) here https://channels.readthedocs.io/en/stable/topics/consumers.html#basic-layout

from channels.consumer import SyncConsumer

class EchoConsumer(SyncConsumer):

    def websocket_connect(self, event):
        self.send({
            "type": "websocket.accept",
        })

    def websocket_receive(self, event):
        self.send({
            "type": "websocket.send",
            "text": event["text"],
        })

Consumers are structured around a series of named methods corresponding to the type value of the messages they are going to receive, with any . replaced by _. The two handlers above are handling websocket.connect and websocket.receive messages respectively.

However when I was really using it in my django app, the . (dot) is not working (at times) and I have to resort to exact match (putting _ instead of . in type). I am really confused.

I agree that the docs should stress more about this rules. How can we improve the docs (and help newcomers like me)? Thanks.

c04022004 avatar Jun 02 '21 02:06 c04022004

I started researching because the example I was following gives me BackgroundTaskConsumer() takes no arguments. I'm facing this exact confusion.

  • I can't find the right combo of dots, underscores and method names for my consumer method to be called
  • I'm not sure what to send
  • I'm not sure what my routing should be
  • I'm not sure what my runworker channel should be

I would love a complete example with send, routing, consumer and worker command. I'm not sure why the example leaps to test.print. If I discover the answer I'll leave it here.

lukerohde avatar Mar 10 '23 07:03 lukerohde

This is what got working using channels 3.0.1, django 3.1.14 and daphne 3.0.2. In this example a message sent to the background-tasks channel gets routed to the method task_b in BackgroundTaskConsumer.

I send a message to my task like this

async_to_sync(channel_layer.send)('background-tasks', {'type': 'task_b', 'id': self.user.id})

the routing in my asgi.py looks like this. I was missing the parenthesis on BackgroundTaskConsumer()

import os

from channels.auth import AuthMiddlewareStack
from channels.routing import ChannelNameRouter,ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chat.settings")
django_asgi_app = get_asgi_application()

import core.routing
from task.consumers import BackgroundTaskConsumer

application = ProtocolTypeRouter(
    {
        "http": django_asgi_app,
        "websocket": AllowedHostsOriginValidator(
            AuthMiddlewareStack(URLRouter(core.routing.websocket_urlpatterns))
        ),
        'channel': ChannelNameRouter({
            'background-tasks': BackgroundTaskConsumer(),
        })
    }
)

My consumer looks like this

from time import sleep
from channels.consumer import AsyncConsumer

class BackgroundTaskConsumer(AsyncConsumer):
    async def task_a(self, message):
        print('sleeping a' + "{}".format(message))
        sleep(5)
        print('waking a')

    async def task_b(self, message):
        print('sleeping b' + "{}".format(message))
        sleep(5)
        print('waking b')

I run my worker like this

python manage.py runworker background-tasks

When a message is sent, the worker outputs

sleeping b{'type': 'task_b', 'id': 1}
waking b

lukerohde avatar Mar 10 '23 09:03 lukerohde