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. Base class for all channel layer implementations.
""" """
@ -51,3 +51,12 @@ class Channel(object):
response channels. response channels.
""" """
raise NotImplementedError() 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 string
import random import random
from collections import deque from collections import deque
from .base import Channel as BaseChannel from .base import BaseChannel
queues = {} queues = {}
closed = set() closed = set()
class Channel(BaseChannel): class InMemoryChannel(BaseChannel):
""" """
In-memory channel implementation. Intended only for use with threading, In-memory channel implementation. Intended only for use with threading,
in low-throughput development environments. 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 import threading
from django.core.management.commands.runserver import Command as RunserverCommand from django.core.management.commands.runserver import Command as RunserverCommand
from django.core.handlers.wsgi import WSGIHandler from django.core.handlers.wsgi import WSGIHandler
from channel import Channel, coreg from django.http import HttpResponse
from channel.request import encode_request from channels import Channel, coreg
from channel.response import decode_response from channels.worker import Worker
from channel.worker import Worker from channels.utils import auto_import_consumers
from channel.utils import auto_import_consumers from channels.adapters import UrlConsumer
class Command(RunserverCommand): class Command(RunserverCommand):
@ -24,7 +24,8 @@ class Command(RunserverCommand):
# Check a handler is registered for http reqs # Check a handler is registered for http reqs
auto_import_consumers() auto_import_consumers()
if not coreg.consumer_for_channel("django.wsgi.request"): 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 # Launch a worker thread
worker = WorkerThread() worker = WorkerThread()
worker.daemon = True worker.daemon = True
@ -39,13 +40,10 @@ class WSGIInterfaceHandler(WSGIHandler):
""" """
def get_response(self, request): def get_response(self, request):
response_channel = Channel.new_name("django.wsgi.response") request.response_channel = Channel.new_name("django.wsgi.response")
Channel("django.wsgi.request").send( Channel("django.wsgi.request").send(**request.channel_encode())
request = encode_request(request), channel, message = Channel.receive_many([request.response_channel])
response_channel = response_channel, return HttpResponse.channel_decode(message)
)
channel, message = Channel.receive_many([response_channel])
return decode_response(message)
class WorkerThread(threading.Thread): 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": request.path,
"path_info": request.path_info, "path_info": request.path_info,
"method": request.method, "method": request.method,
"response_channel": request.response_channel,
} }
return value return value
@ -31,4 +32,5 @@ def decode_request(value):
request.path = value['path'] request.path = value['path']
request.method = value['method'] request.method = value['method']
request.path_info = value['path_info'] request.path_info = value['path_info']
request.response_channel = value['response_channel']
return request 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']} response._headers = {k.lower: (k, v) for k, v in value['headers']}
return response 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: try:
__import__(consumer_module_name) __import__(consumer_module_name)
except ImportError as e: except ImportError as e:
if "no module named" not in str(e).lower(): if "no module named consumers" not in str(e).lower():
raise raise

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