|
3 | 3 | Live updating
|
4 | 4 | =============
|
5 | 5 |
|
6 |
| -Live updating blah. |
| 6 | +Live updating is supported using additional ``Dash`` :ref:`components <dash_components>` and |
| 7 | +leveraging `Django Channels <https://channels.readthedocs.io/en/latest/>`_ to provide websocket endpoints. |
| 8 | + |
| 9 | +Server-initiated messages are sent to all interested clients. The content of the message is then injected into |
| 10 | +the application from the client, and from that point it is handled like any other value passed to a callback function. |
| 11 | +The messages are constrained to be JSON serialisable, as that is how they are transmitted to and from the clients, and should |
| 12 | +also be as small as possible given that they travel from the server, to each interested client, and then back to the |
| 13 | +server again as an argument to one or more callback functions. |
| 14 | + |
| 15 | +The round-trip of the message is a deliberate design choice, in order to enable the value within the message to be treated |
| 16 | +as much as possible like any other piece of data within a ``Dash`` application. This data is essentially stored |
| 17 | +on the client side of the client-server split, and passed to the server when each callback is invoked; note that this also |
| 18 | +encourages designs that keep the size of in-application data small. An |
| 19 | +alternative approach, such as directly invoking |
| 20 | +a callback in the server, would require the server to maintain its own copy of the application state. |
| 21 | + |
| 22 | +Live updating requires a server setup that is considerably more |
| 23 | +complex than the alternative, namely use of the built-in `Interval <https://dash.plot.ly/live-updates>`_ component. However, live |
| 24 | +updating can be used to reduce server load (as callbacks are only made when needed) and application latency (as callbacks are |
| 25 | +invoked as needed, not on the tempo of the Interval component). |
7 | 26 |
|
8 | 27 | Message channels
|
9 | 28 | ----------------
|
10 | 29 |
|
11 |
| -Blah |
| 30 | +Messages are passed through named channels, and each message consists |
| 31 | +of a ``label`` and ``value`` pair. A :ref:`Pipe <pipe_component>` component is provided that listens for messages and makes |
| 32 | +them available to ``Dash`` callbacks. Each message is sent through a message channel to all ``Pipe`` components that have |
| 33 | +registered their interest in that channel, and in turn the components will select messages by ``label``. |
| 34 | + |
| 35 | +A message channel exists as soon as a component signals that it is listening for messages on it. The |
| 36 | +message delivery requirement is 'hopefully at least once'. In other words, applications should be robust against both the failure |
| 37 | +of a message to be delivered, and also for a message to be delivered multiple times. A design approach that has messages |
| 38 | +of the form 'you should look at X and see if something should be done' is strongly encouraged. The accompanying demo has |
| 39 | +messages of the form 'button X at time T', for example. |
| 40 | + |
| 41 | +Sending messages from within Django |
| 42 | +----------------------------------- |
| 43 | + |
| 44 | +Messages can be easily sent from within Django, provided that they are within the ASGI server. |
| 45 | + |
| 46 | +.. code-block:: python |
| 47 | +
|
| 48 | + from django_plotly_dash.consumers import send_to_pipe_channel |
| 49 | +
|
| 50 | + # Send a message |
| 51 | + # |
| 52 | + # This function may return *before* the message has been sent |
| 53 | + # to the pipe channel. |
| 54 | + # |
| 55 | + send_to_pipe_channel(channel_name="live_button_counter", |
| 56 | + label="named_counts", |
| 57 | + value=value) |
| 58 | +
|
| 59 | + # Send a message asynchronously |
| 60 | + # |
| 61 | + await async_send_to_pipe_channel(channel_name="live_button_counter", |
| 62 | + label="named_counts", |
| 63 | + value=value) |
| 64 | +
|
| 65 | +In general, making assumptions about the ordering of code between message sending and receiving is |
| 66 | +unsafe. The ``send_to_pipe`` function uses the Django Channels ``async_to_sync`` wrapper around |
| 67 | +a call to ``async_send_to_pipe`` and therefore may return before the asynchronous call is made (perhaps |
| 68 | +on a different thread). Furthermore, the transit of the message through the channels backend |
| 69 | +introduces another indeterminacy. |
| 70 | + |
| 71 | +HTTP Endpoint |
| 72 | +------------- |
| 73 | + |
| 74 | +There is an HTTP endpoint, :ref:`configured <configuration>` with |
| 75 | +the ``http_route`` option, that allows direct insertion of messages into a message |
| 76 | +channel. It is a |
| 77 | +direct equivalent of calling the ``send_to_pipe_channel`` function, and |
| 78 | +expects the ``channel_name``, ``label`` and ``value`` arguments to be provided in a JSON-encoded |
| 79 | +dictionary. |
| 80 | + |
| 81 | +.. code-block:: bash |
| 82 | +
|
| 83 | + curl -d '{"channel_name":"live_button_counter", |
| 84 | + "label":"named_counts", |
| 85 | + "value":{"click_colour":"cyan"}}' |
| 86 | + http://localhost:8000/dpd/views/poke/ |
| 87 | +
|
| 88 | +This will cause the (JSON-encoded) ``value`` argument to be sent on the ``channel_name`` channel with |
| 89 | +the given ``label``. |
| 90 | + |
| 91 | +The provided endpoint skips any CSRF checks |
| 92 | +and does not perform any security checks such as authentication or authorisation, and should |
| 93 | +be regarded as a starting point for a more complete implementation if exposing this functionality is desired. On the |
| 94 | +other hand, if this endpoint is restricted so that it is only available from trusted sources such as the server |
| 95 | +itself, it does provide a mechanism for Django code running outside of the ASGI server, such as in a WSGI process or |
| 96 | +Celery worker, to push a message out to running applications. |
12 | 97 |
|
13 |
| -Pipes |
14 |
| ------ |
| 98 | +The ``http_poke_enabled`` flag controls the availability of the endpoint. If false, then it is not registered at all and |
| 99 | +all requests will receive a 404 HTTP error code. |
15 | 100 |
|
16 |
| -A :ref:`Pipe <pipe_component>` component is provided. |
| 101 | +Deployment |
| 102 | +---------- |
17 | 103 |
|
| 104 | +The live updating feature needs both Redis, as it is the only supported backend at present for v2.0 and up of |
| 105 | +Channels, and Daphne or any other ASGI server for production use. It is also good practise to place the server(s) behind |
| 106 | +a reverse proxy such as Nginx; this can then also be configured to serve Django's static files. |
18 | 107 |
|
| 108 | +A further consideration is the use of a WSGI server, such as Gunicorn, to serve the non-asynchronous subset of the http |
| 109 | +routes, albeit at the expense of having to separately manage ASGI and WSGI servers. This can be easily achieved through selective |
| 110 | +routing at the reverse proxy level, and is the driver behind the ``ws_route`` configuration option. |
19 | 111 |
|
| 112 | +In passing, note that the demo also uses Redis as the caching backend for Django. |
0 commit comments