Rename to plural "channels", start fleshing out req/resp cycle

This commit is contained in:
Andrew Godwin 2015-06-08 10:51:51 -07:00
parent 821816f656
commit 6cd01e2bc1
19 changed files with 168 additions and 42 deletions

View File

@ -1,7 +0,0 @@
from .consumer_registry import ConsumerRegistry
# Make a site-wide registry
coreg = ConsumerRegistry()
# Load an implementation of Channel
from .channels.memory import Channel

View File

@ -1,18 +0,0 @@
from django.core.handlers.base import BaseHandler
from channel import Channel
from .response import encode_response
from .request import decode_request
class DjangoUrlAdapter(object):
"""
Adapts the channel-style HTTP requests to the URL-router/handler style
"""
def __init__(self):
self.handler = BaseHandler()
self.handler.load_middleware()
def __call__(self, request, response_channel):
response = self.handler.get_response(decode_request(request))
Channel(response_channel).send(**encode_response(response))

11
channels/__init__.py Normal file
View File

@ -0,0 +1,11 @@
from .consumer_registry import ConsumerRegistry
# Make a site-wide registry
coreg = ConsumerRegistry()
# Load an implementation of Channel
from .backends import InMemoryChannel as Channel
# Ensure monkeypatching
from .hacks import monkeypatch_django
monkeypatch_django()

47
channels/adapters.py Normal file
View File

@ -0,0 +1,47 @@
from django.core.handlers.base import BaseHandler
from django.http import HttpRequest, HttpResponse
from channels import Channel, coreg
class UrlConsumer(object):
"""
Dispatches channel HTTP requests into django's URL system.
"""
def __init__(self):
self.handler = BaseHandler()
self.handler.load_middleware()
def __call__(self, **kwargs):
request = HttpRequest.channel_decode(kwargs)
try:
response = self.handler.get_response(request)
except HttpResponse.ResponseLater:
return
Channel(request.response_channel).send(**response.channel_encode())
def view_producer(channel_name):
"""
Returns a new view function that actually writes the request to a channel
and abandons the response (with an exception the Worker will catch)
"""
def producing_view(request):
Channel(channel_name).send(**request.channel_encode())
raise HttpResponse.ResponseLater()
return producing_view
def view_consumer(channel_name):
"""
Decorates a normal Django view to be a channel consumer.
Does not run any middleware.
"""
def inner(func):
def consumer(**kwargs):
request = HttpRequest.channel_decode(kwargs)
response = func(request)
Channel(request.response_channel).send(**response.channel_encode())
coreg.add_consumer(consumer, [channel_name])
return func
return inner

View File

@ -0,0 +1,2 @@
from .base import BaseChannel
from .memory import InMemoryChannel

View File

@ -1,4 +1,4 @@
class Channel(object):
class BaseChannel(object):
"""
Base class for all channel layer implementations.
"""
@ -51,3 +51,12 @@ class Channel(object):
response channels.
"""
raise NotImplementedError()
def as_view(self):
"""
Returns a view version of this channel - one that takes
the request passed in and dispatches it to our channel,
serialized.
"""
from channels.adapters import view_producer
return view_producer(self.name)

View File

@ -2,12 +2,12 @@ import time
import string
import random
from collections import deque
from .base import Channel as BaseChannel
from .base import BaseChannel
queues = {}
closed = set()
class Channel(BaseChannel):
class InMemoryChannel(BaseChannel):
"""
In-memory channel implementation. Intended only for use with threading,
in low-throughput development environments.

View File

View File

@ -0,0 +1,23 @@
Message Standards
=================
Some standardised message formats are used for common message types - they
are detailed below.
HTTP Request
------------
Represents a full-fledged, single HTTP request coming in from a client.
Contains the following keys:
* request: An encoded Django HTTP request
* response_channel: The channel name to write responses to
HTTP Response
-------------
Sends a whole response to a client.
Contains the following keys:
* response: An encoded Django HTTP response

View File

@ -0,0 +1,23 @@
Message Standards
=================
Some standardised message formats are used for common message types - they
are detailed below.
HTTP Request
------------
Represents a full-fledged, single HTTP request coming in from a client.
Contains the following keys:
* request: An encoded Django HTTP request
* response_channel: The channel name to write responses to
HTTP Response
-------------
Sends a whole response to a client.
Contains the following keys:
* response: An encoded Django HTTP response

27
channels/hacks.py Normal file
View File

@ -0,0 +1,27 @@
from django.http.request import HttpRequest
from django.http.response import HttpResponseBase
from django.core.handlers.base import BaseHandler
from .request import encode_request, decode_request
from .response import encode_response, decode_response, ResponseLater
def monkeypatch_django():
"""
Monkeypatches support for us into parts of Django.
"""
# Request encode/decode
HttpRequest.channel_encode = encode_request
HttpRequest.channel_decode = staticmethod(decode_request)
# Response encode/decode
HttpResponseBase.channel_encode = encode_response
HttpResponseBase.channel_decode = staticmethod(decode_response)
HttpResponseBase.ResponseLater = ResponseLater
# Allow ResponseLater to propagate above handler
BaseHandler.old_handle_uncaught_exception = BaseHandler.handle_uncaught_exception
BaseHandler.handle_uncaught_exception = new_handle_uncaught_exception
def new_handle_uncaught_exception(self, request, resolver, exc_info):
if exc_info[0] is ResponseLater:
raise
return BaseHandler.old_handle_uncaught_exception(self, request, resolver, exc_info)

View File

@ -2,11 +2,11 @@ import django
import threading
from django.core.management.commands.runserver import Command as RunserverCommand
from django.core.handlers.wsgi import WSGIHandler
from channel import Channel, coreg
from channel.request import encode_request
from channel.response import decode_response
from channel.worker import Worker
from channel.utils import auto_import_consumers
from django.http import HttpResponse
from channels import Channel, coreg
from channels.worker import Worker
from channels.utils import auto_import_consumers
from channels.adapters import UrlConsumer
class Command(RunserverCommand):
@ -24,7 +24,8 @@ class Command(RunserverCommand):
# Check a handler is registered for http reqs
auto_import_consumers()
if not coreg.consumer_for_channel("django.wsgi.request"):
raise RuntimeError("No consumer registered for WSGI requests")
# Register the default one
coreg.add_consumer(UrlConsumer(), ["django.wsgi.request"])
# Launch a worker thread
worker = WorkerThread()
worker.daemon = True
@ -39,13 +40,10 @@ class WSGIInterfaceHandler(WSGIHandler):
"""
def get_response(self, request):
response_channel = Channel.new_name("django.wsgi.response")
Channel("django.wsgi.request").send(
request = encode_request(request),
response_channel = response_channel,
)
channel, message = Channel.receive_many([response_channel])
return decode_response(message)
request.response_channel = Channel.new_name("django.wsgi.response")
Channel("django.wsgi.request").send(**request.channel_encode())
channel, message = Channel.receive_many([request.response_channel])
return HttpResponse.channel_decode(message)
class WorkerThread(threading.Thread):

2
channel/request.py → channels/request.py Executable file → Normal file
View File

@ -15,6 +15,7 @@ def encode_request(request):
"path": request.path,
"path_info": request.path_info,
"method": request.method,
"response_channel": request.response_channel,
}
return value
@ -31,4 +32,5 @@ def decode_request(value):
request.path = value['path']
request.method = value['method']
request.path_info = value['path_info']
request.response_channel = value['response_channel']
return request

9
channel/response.py → channels/response.py Executable file → Normal file
View File

@ -27,3 +27,12 @@ def decode_response(value):
)
response._headers = {k.lower: (k, v) for k, v in value['headers']}
return response
class ResponseLater(Exception):
"""
Class that represents a response which will be sent doown the response
channel later. Used to move a django view-based segment onto the next
task, as otherwise we'd need to write some kind of fake response.
"""
pass

2
channel/utils.py → channels/utils.py Executable file → Normal file
View File

@ -10,5 +10,5 @@ def auto_import_consumers():
try:
__import__(consumer_module_name)
except ImportError as e:
if "no module named" not in str(e).lower():
if "no module named consumers" not in str(e).lower():
raise

0
channel/worker.py → channels/worker.py Executable file → Normal file
View File