question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Guidance on ASGI extensions

See original GitHub issue

I have niche use-case for an ASGI extension, and I’m trying to understand if creating a custom extension is the correct approach. I was going to add a comment to the issue here because there is some relevant discussion, but this became rather long and I didn’t want to add unnecessary noise to that one.

I’ll be referring to my specific use-case and custom extension in this issue, however, my goal is to achieve a broader understanding of how new ASGI extensions should be created and their appropriate level of abstraction which I think might be useful to others as the ecosystem grows and extensions are used more often.

Background

I’m attempting to implement a publish-subscribe pattern for WebSocket broadcast for a serverless ASGI adapter. The limitations of this environment require it to be implemented in a particular way that is different than a normal ASGI protocol server.

Essentially, a persistent WebSocket connection is established with a client, but the connection scope must be stored in a remote data source to run the ASGI application correctly. The scope is re-used/updated to create a new ASGI application instance for subsequent message events until the connection is closed.

Extension

I’ve come at this problem from a few different angles, but the simplest approach I’ve found so far is to create a custom extension for WebSocket broadcast that is advertised using websocket.broadcast as the key with the subscription channels stored in the extra scope information:

"scope": {
    ...
    "extensions": {"websocket.broadcast": {"subscriptions": []}}
}

My protocol implementation uses this information to ensure the following:

  • Subscriptions in the data source are updated when a new client is subscribed
  • New messages are published to subscribers
  • Applications are able to unsubscribe clients
  • Subscriptions are removed when a connection is closed

There are three events handled by the protocol:

Subscribe

The protocol server subscribes the current client connection to the channel by adding a connection identifer to the data source and appending it to the subscriptions in the scope.

Keys:

type (Unicode string): "websocket.broadcast.subscribe" channel (Unicode string): The name of the subscription channel to subscribe.

Unsubscribe

The protocol server unsubscribes the current client connection from the channel by removing the subscription from the data source and from the connection scope.

Keys:

type (Unicode string): "websocket.broadcast.unsubscribe" channel (Unicode string): The name of the subscription channel to unsubscribe.

Publish

Keys:

type (Unicode string): "websocket.broadcast.publish" channel (Unicode string): The name of the channel to publish the message. body (bytes): The message content.

Problem

I am developing this against an example Starlette application, which I’ll use to demonstrate the problem I’m facing, but my concern is about extension support in ASGI framework implementations generally.

The extension approach requires access to the private _send method of Starlette’s WebSocket implementation (the public send only supports standard WebSocket message events):

async def on_subscribe(self, websocket, message):
    channel = message["channel"]
    await websocket._send(
        {"type": "websocket.broadcast.subscribe", "channel": channel}
    )

This seems incorrect and might be confusing to users, my alternative is to implement a middleware for the protocol server and implement it this way in the application:

async def on_subscribe(self, websocket, message) -> None:
    channel = message["channel"]
    await websocket.send_json({"type": "broadcast.subscribe", "channel": channel})

Which is more universally applicable across frameworks, but adds a significant amount of complexity to the protocol server.

Question

I’ve asked elsewhere and the feedback has been that this isn’t the appropriate layer to implement an extension at. The discussion in the issue I mentioned previously seems to echo concerns about how narrowly-defined an extension should be, so I thought it might be good to open a discussion about extensions generally.

My specific questions:

  1. Is this the appropriate usage of extensions?

  2. Is there a more generic way to accomodate this case that could also be of use to other ASGI servers? If an ASGI server really wanted to it could implement this extension, though I’m not sure if that’d ever be preferred over something like broadcaster.

  3. Could there be some additional guidance added to the spec/docs about creating new extensions? I’d be happy to PR this myself based on any feedback in this issue, if desired.

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:6 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
jordaneremieffcommented, Jun 4, 2020

Thanks @tomchristie!

Broadcaster is really awesome, and the pattern you’ve implemented there is wonderful. I use this approach here to define the common interface for the backend sub-classes, and then configure it here based on a setting provided. This is necessary for my use-case (with or without broadcast support) because of the requirement to persist the connection scope, but I decided to re-use it also for the broadcast support since a backend is already required for basic WebSocket support.

However, since the AWS Lambda environment is stateless the broadcast implementation has to be approached differently than how broadcaster does it. I am unable to use the extended functionality provided by any of the backends given this limitation, so the design I settled on was to provide only an interface for setting/retrieving the connection identifiers for a particular channel and sending the data to connected clients (which then triggers their next ASGI WebSocket cycle).

My intention with the adapter is to provide the bare minimum to establish the necessary messaging interface in this environment and leave up anything more extensive (window history, etc.) to the application.

0reactions
andrewgodwincommented, Jun 14, 2020

I don’t think that would violate it - ASGI still follows the design principles laid down in Channels, which is that you’re free to write multiplexing layers. Note that the scope is specifically called out as immutable once it’s passed through a middleware precisely so lower-down apps can do their own things with it.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Extensions — ASGI 3.0 documentation - Read the Docs
The ASGI specification provides for server-specific extensions to be used outside of the core ASGI specification. This document specifies some common extensions ......
Read more >
An Introduction to ASGI, Asynchronous Server Gateway Interface
If you develop web applications in Python you will almost certainly be doing so using WSGI, with the most popular frameworks, ...
Read more >
How To Set Up an ASGI Django App with Postgres, Nginx ...
In this guide, you will set up an ASGI (Asynchronous Server Gateway ... should match the socket filename exception for the extension:.
Read more >
Integration Guide - Python ASGI (FastAPI) | Moesif Docs
If you are using a framework that is built on top of ASGI, it should work just by adding the Moesif middleware. Please...
Read more >
encode/community - Gitter
The ASGI extension approach simplified all of this into a matter of checking the message type and using the subscriptions key to ensure...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found