Django Channels, ASGI, and Heroku

Getting it up and running

Deploying to Heroku

  1. uvicorn doesn’t pull from $PORT. You must specify --port $PORT or it won’t boot. I specified --host 0.0.0.0 while I was at it just in case, but it might be required.
  2. uvicorn doesn’t have an equivalent to --max-requests-jitter in gunicorn. --max-requests in gunicorn allows you to restart a worker after some number of requests, and this is available in uvicorn as --limit-max-requests. Without the jitter option, though, you potentially set yourself up for thundering herds of server reboots. That’s not a problem for a project with no traffic (like mine), but I couldn’t for instance deploy it to Pinecast in its current state. This is perhaps a good open source contribution that I could make in the coming weeks.

Making it run

app[web.1]: File "/app/.heroku/python/lib/python3.7/site-packages/django/utils/asyncio.py", line 26, in inner
app[web.1]: return func(*args, **kwargs)
app[web.1]: File "/app/.heroku/python/lib/python3.7/site-packages/django/db/backends/postgresql/base.py", line 185, in get_new_connection
app[web.1]: connection = Database.connect(**conn_params)
app[web.1]: File "/app/.heroku/python/lib/python3.7/site-packages/psycopg2/__init__.py", line 130, in connect
app[web.1]: conn = _connect(dsn, connection_factory=connection_factory, **kwasync)
app[web.1]: django.db.utils.OperationalError: FATAL: too many connections for role "gihouwrmsatwpc"
>>> import multiprocessing
>>> multiprocessing.cpu_count()
8

Realtime

import graphene
from graphene_subscriptions.events import CREATED
from ..models import Message # Our Django model for messages
from .message_type import MessageType # Our Graphene Message type
# Our GraphQL subscription class
class Subscription(graphene.ObjectType):
# Define what the user can subscribe to in our schema
message_created = graphene.Field(
graphene.NonNull(MessageType),
room=graphene.Argument(graphene.ID, required=True),
)
def resolve_message_created(root, info, **kw):
def filter(event):
return (
event.operation == CREATED and
isinstance(event.instance, Message) and
event.instance.room_id == kw.get('room')
)
return root.filter(filter).map(lambda e: e.instance)
subscription MessageCreated($room: ID!) {
messageCreated(room: $room) {
id
created
messageText
sender {
id
name
avatar(size: 32)
}
}
}
def resolve_message_created(root, info, **kw):
def filter(event):
return (
event.operation == CREATED and
isinstance(event.instance, Message) and
event.instance.room_id == kw.get('room') and
# nice and safe
check_user_has_access_to_room(kw.get('room'))
)
return root.filter(filter).map(lambda e: e.instance)

Making it work with Apollo

const wsLink = new WebSocketLink({
uri: `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${
window.location.host
}/graphql`,
options: {
reconnect: true,
},
});
import {split} from 'apollo-link';const link = split(
({query}) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink, // Our new web socket link
httpLink, // Our existing HTTP link
);
ValueError: Django can only handle ASGI/HTTP connections, not websocket.
diff --git a/yourapp/asgi.py b/yourapp/asgi.py
index eb61b00..19e073e 100644
--- a/yourapp/asgi.py
+++ b/yourapp/asgi.py
@@ -9,8 +9,9 @@

import os

-from django.core.asgi import get_asgi_application
+import django
+from channels.routing import get_default_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yourapp.settings')
-
-application = get_asgi_application()
+django.setup()
+application = get_default_application()

Making it work on Heroku

ERROR:    An error occurred while resolving field Mutations.createMessage
Traceback (most recent call last):
File "[...]/site-packages/graphql/execution/executor.py", line 452, in resolve_or_error
return executor.execute(resolve_fn, source, info, **args)
[...]File "/app/messaging/schema/mutations/create_message.py", line 68, in mutate
message.save()
[...]File "[...]/site-packages/aioredis/connection.py", line 322, in execute
raise ConnectionClosedError(msg)
aioredis.errors.ConnectionClosedError: Reader at end of file
  • The problem seems to be resolved by upgrading to the latest Redis channel layer. I was already on the latest version, so this was not the issue.
  • The problem seems to be mostly unique to Heroku (though some folks have experienced it locally), which indicates a configuration issue or bug in Heroku’s Redis implementation.
  • Folks have reported success when switching from Heroku’s first-party Redis offering to Redis Cloud (another Heroku add-on). Some folks reported intermittent issues even after moving to Redis Cloud, though.
$ heroku redis:timeout [your-redis-instance-0000] --seconds=0
Timeout for [your-redis-instance-0000] (REDIS_URL) set to 0 seconds.
Connections to the Redis instance can idle indefinitely.

Making it production-ready

$ heroku buildpacks:add -i 1 heroku/redis
web: bin/start-stunnel uvicorn ironcoach.asgi:application --limit-max-requests=1200 --port $PORT
# Remove this:
FOO = os.environ.get('REDIS_URL')
# And do this:
REDIS_URL = os.environ.get('REDIS_URL_STUNNEL') or \
os.environ.get('REDIS_URL')
FOO = REDIS_URL
-----> stunnel app detected
-----> Moving the configuration generation script into app/bin
-----> Moving the start-stunnel script into app/bin
-----> stunnel done

Other notes

graphene-subscriptions only supports one group

stunnel crashes on startup

INTERNAL ERROR: systemd initialization failed at stunnel.c, line 101

What’s next?

A salty software guy. Stripe by day, Pinecast by night.

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

💲 Your PIXEL balance: 250 PIXEL. You have invited 0 user(s).

Making a RTS game #4: Selecting units (Unity/C#)

Running Cicada Distributed Tests in Kubernetes

Flutter vs React Native vs Ionic. What is your choice and why?

Humans of HackBeanpot: Brandon Liang

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Matt Basta

Matt Basta

A salty software guy. Stripe by day, Pinecast by night.

More from Medium

[Django][Restframework] Filter data in Django Rest Framework

Simple REST API application using Flask

Deploy Django App in AWS EC2 Instance Using Docker Image

Django-crispy-forms: Clean Horizontal Multiple Choice Fields