Slide 1

Slide 1 text

A QUICK PRIMER ON CHANNELS A QUICK PRIMER ON CHANNELS BUILDING REAL TIME BUILDING REAL TIME APPLICATIONS WITH APPLICATIONS WITH DJANGO DJANGO DjangoCon Europe 2018

Slide 2

Slide 2 text

HELLO, I AM IACOPO HELLO, I AM IACOPO Founder and CTO @NephilaIt Djangonaut and django CMS core developer @yakkys @[email protected] https://keybase.io/yakky https://github.com/yakky

Slide 3

Slide 3 text

REALTIME WEB REALTIME WEB No more pure request-response web Lot of tools (nice!) Lots of complexity (booo!) Why not doing it the Django-way?

Slide 4

Slide 4 text

SO, CHANNELS SO, CHANNELS Framework to use Django in non-HTTP world websockets any protocol (bots, IoT, ...)

Slide 5

Slide 5 text

SO, CHANNELS SO, CHANNELS CHANNELS VERSIONS CHANNELS VERSIONS Channels 2 is the shiny present and future Channels 1 served for almost three years Will focus on 2 (2.1, really)

Slide 6

Slide 6 text

TALK OUTLINE TALK OUTLINE Channels concepts Demo! Code drill down

Slide 7

Slide 7 text

CONCEPTS CONCEPTS

Slide 8

Slide 8 text

CONCEPTS CONCEPTS ASYNCHRONOUS ASYNCHRONOUS event-driven async code is more complex to understand Channels 2: best of both worlds

Slide 9

Slide 9 text

CONCEPTS CONCEPTS ASGI ASGI Protocol specs

Slide 10

Slide 10 text

CONCEPTS CONCEPTS ASGI-ALL-THE-WAY-DOWN ASGI-ALL-THE-WAY-DOWN

Slide 11

Slide 11 text

CONCEPTS CONCEPTS PROTOCOL SERVER PROTOCOL SERVER Implements the ASGI specs for a specific protocol Bridge between the network and the application (e.g.: Daphne for HTTP/websockets)

Slide 12

Slide 12 text

CONCEPTS CONCEPTS ROUTING ROUTING Map incoming messages to consumers nestable and composable

Slide 13

Slide 13 text

CONCEPTS CONCEPTS SCOPE SCOPE Connection data structure Connection ↔ Scope ↔ Application (consumer) instance

Slide 14

Slide 14 text

CONCEPTS CONCEPTS CHANNEL CHANNEL Channels IPC mechanism A FIFO at-most-once queue

Slide 15

Slide 15 text

CONCEPTS CONCEPTS CONSUMERS CONSUMERS Core abstraction to build ASGI app Handle events on a connection Stateful

Slide 16

Slide 16 text

CONCEPTS CONCEPTS GROUP GROUP Group consumer instances on channel layer

Slide 17

Slide 17 text

CONCEPTS CONCEPTS EVENTS EVENTS Triggered during a scope Consumed by consumers

Slide 18

Slide 18 text

CONCEPTS CONCEPTS FRONTEND FRONTEND Websockets means Javascript Channels ships a lightweight js library

Slide 19

Slide 19 text

CONCEPTS CONCEPTS HIGHLIGHTS HIGHLIGHTS Fully exposed async Based on asyncio Mix of sync / async code Single process In-process protocol termination No separate worker required Middlewares ASGI-all-the-way-down

Slide 20

Slide 20 text

DEMO TIME! DEMO TIME! Enough theory

Slide 21

Slide 21 text

DEMO TIME DEMO TIME EXAMPLE APPLICATION EXAMPLE APPLICATION https://github.com/yakky/channeled-dashboard

Slide 22

Slide 22 text

DEMO TIME DEMO TIME FEATURES FEATURES Count active users Concurrency check Browser notifications

Slide 23

Slide 23 text

DEMO TIME DEMO TIME 0:00 / 1:05

Slide 24

Slide 24 text

DIVE IN DIVE IN Channel layers Routing Consumers

Slide 25

Slide 25 text

DIVE IN DIVE IN CHANNEL LAYERS CHANNEL LAYERS ASGI_APPLICATION = 'dashboard.routing.application' CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { 'hosts': [('localhost', 6379)], }, }, }

Slide 26

Slide 26 text

DIVE IN DIVE IN ROUTING ROUTING # equivalent to my_project.urls # tipically used to include application routing application = ProtocolTypeRouter({ 'websocket': AuthMiddlewareStack( URLRouter([ path('status/', documents_routing), ]) ), })

Slide 27

Slide 27 text

ROUTING ROUTING ROUTER TYPES ROUTER TYPES # routing is established on connection start application = ProtocolTypeRouter({ 'websocket': AuthMiddlewareStack( URLRouter([ path('status/', documents_routing), ]) ), }) Middlewares Routers A routing, is forever

Slide 28

Slide 28 text

ROUTING ROUTING APPLICATION ROUTING APPLICATION ROUTING channel_routing = URLRouter([ path('users/', UserCounterConsumer), path('documents/', DocumentListConsumer), path('document///', DocumentDetailConsum ])

Slide 29

Slide 29 text

DIVE IN DIVE IN CONSUMERS CONSUMERS

Slide 30

Slide 30 text

CONSUMERS CONSUMERS GENERIC WEBSOCKET GENERIC WEBSOCKET class SomeConsumer(WebsocketConsumer): """ Generic Websocket consumer """ groups = ['some_group', 'other'] def connect(self): ... def disconnect(self, code): ... def receive(self, text_data=None, bytes_data=None): ...

Slide 31

Slide 31 text

CONSUMERS CONSUMERS APPLICATION EVENTS APPLICATION EVENTS class UserCounterConsumer(JsonWebsocketConsumer): def connect(self): # this put a message in group users of the channel layer # type is the event to be generated async_to_sync(self.channel_layer.group_send)( 'users', {'type': 'users.count', 'value': 5} ) def users_count(self, event): # this catches the 'users.count' event / message # and send it to its connected client # it runs on all instances connected to the 'users' group self.send_json(content=event['value'])

Slide 32

Slide 32 text

CONSUMERS CONSUMERS COUNTING USERS COUNTING USERS

Slide 33

Slide 33 text

CONSUMERS CONSUMERS COUNTING USERS COUNTING USERS class UserCounterConsumer(JsonWebsocketConsumer): groups = 'users', def connect(self): """ Increment users on connect and notify other consumers super().connect() if self.scope['user'].is_authenticated: increment_users(message.user) msg = {'users': count_users(), 'type': 'users.count'} async_to_sync(self.channel_layer.group_send)('users', msg def users_count(self, event): """ Notify connected user """ self.send_json(content=event['message']) def disconnect(self, code): """ Decrement users on disconnect and notify users """ ...

Slide 34

Slide 34 text

COUNTING USERS COUNTING USERS FRONTEND FRONTEND if (users_count_path) { // Library is instantiated and connected to the endpoint const users_count = new channels.WebSocketBridge(); users_count.connect(users_count_path); users_count.listen((data) => { if (data.users) { document.getElementById( 'users-counter' ).textContent = data.users; } }); }

Slide 35

Slide 35 text

CONSUMERS CONSUMERS CONCURRENCY MONITOR CONCURRENCY MONITOR

Slide 36

Slide 36 text

CONCURRENCY MONITOR CONCURRENCY MONITOR SINGLE DOCUMENT SINGLE DOCUMENT class DocumentDetailConsumer(DocumentListConsumer): @property def groups(self): return [self.slug, Document.Status.list] def connect(self): self._update_document_count( self.scope['user'].get_full_name() ) super(DocumentDetailConsumer, self).connect() async_to_sync(self.channel_layer.group_send)( self.slug, { 'type': 'document.status', 'message': self.get_status() }) def document_status(self, event): self.send_json(content=event['message'])

Slide 37

Slide 37 text

CONSUMER CONSUMER DETAIL #1 DETAIL #1 def connect(): ... async_to_sync(self.channel_layer.group_send)( self.slug, { ... })

Slide 38

Slide 38 text

CONSUMER CONSUMER DETAIL #2 DETAIL #2 def connect(): ... async_to_sync(...)( self.slug, { 'type': 'document.status', 'message': self.get_status() }) def document_status(self, event): self.send_json(content=event['message'])

Slide 39

Slide 39 text

CONCURRENCY MONITOR CONCURRENCY MONITOR DOCUMENTS LIST DOCUMENTS LIST class DocumentListConsumer(JsonWebsocketConsumer): @property def groups(self): return Document.Status.list, def connect(self): super(DocumentListConsumer, self).connect() async_to_sync(self.channel_layer.group_send)( self.slug, { 'type': 'document.status', 'message': self.get_status_packet() }) def document_status(self, event): self.send_json(content=event['message'])

Slide 40

Slide 40 text

CONCURRENCY MONITOR CONCURRENCY MONITOR FRONTEND FRONTEND // Library is instantiated and connected to the endpoint const document_status = new channels.WebSocketBridge(); document_status.connect(document_path); // whenever a message is received, the documents badges is upda document_status.listen((data) => { if (data.document) { // we only have one document - detail view update_document_detail(data); } else { // all documents - list view update_document_list(data); } });

Slide 41

Slide 41 text

CHANNELS FROM OUTSIDE CHANNELS FROM OUTSIDE CHANNELS CHANNELS class Document(TimeStampedModel): ... def save(self, *args, **kwargs): channel_layer = get_channel_layer() async_to_sync(channel_layer.group_send)(self.slug, { 'type': 'document.saved', 'message': 'document saved' }) return super(Document, self).save(*args, **kwargs) class DocumentDetailConsumer(DocumentListConsumer): ... def document_saved(self, event): do_something(event)

Slide 42

Slide 42 text

DATA BINDING DATA BINDING No more in version 2

Slide 43

Slide 43 text

DATA BINDING DATA BINDING REJOYCE! REJOYCE! An opportunity for the community

Slide 44

Slide 44 text

DATA BINDING DATA BINDING AN EXPERIMENT AN EXPERIMENT resurrecting the dead django-knocker

Slide 45

Slide 45 text

DATA BINDING DATA BINDING KNOCKER KNOCKER based on django-meta hooks into signals sends messages to default consumer

Slide 46

Slide 46 text

KNOCKER KNOCKER OVERVIEW OVERVIEW class Document(KnockerModel, ModelMeta, TimeStampedModel): ... _knocker_data = { 'title': 'get_knocker_title', 'message': 'get_knocker_message', 'icon': 'get_knocker_icon', 'url': 'get_absolute_url', 'language': 'get_knocker_language', } ... def get_knocker_icon(self): ... def get_knocker_messageself): ... def get_knocker_language(self): ...

Slide 47

Slide 47 text

KNOCKER KNOCKER SIGNALS SIGNALS class KnockerModel(object): def __new__(cls, *args, **kwargs): ... new_cls._connect() return new_cls @classmethod def _connect(cls): pre_save.connect(notify_items_pre_save, sender=cls) ...

Slide 48

Slide 48 text

KNOCKER KNOCKER MESSAGES MESSAGES class KnockerModel(object): def send_knock(self, signal_type, created=False): if not self.should_knock(signal_type, created): return knock = self.as_knock(signal_type, created) if knock: channel_layer = get_channel_layer() group = 'knocker-%s' % knock['language'] async_to_sync(channel_layer.group_send)(group, { 'type': 'knocker.saved', 'message': json.dumps(knock) })

Slide 49

Slide 49 text

KNOCKER KNOCKER DEFAULT CONSUMER DEFAULT CONSUMER class KnockerConsumer(JsonWebsocketConsumer): @property def groups(self): lang = self.scope['url_route']['kwargs'].get('language') return 'knocker-%s' % lang, def knocker_saved(self, event): self.send_json(content=event['message'])

Slide 50

Slide 50 text

KNOCKER KNOCKER pretty rough

Slide 51

Slide 51 text

DATA BINDING DATA BINDING WHAT'S NEXT? WHAT'S NEXT? Next databinding library? Maybe talking about this during the sprints?

Slide 52

Slide 52 text

WRAP IT UP WRAP IT UP Async made easier Access to Django API

Slide 53

Slide 53 text

GRAZIE! GRAZIE! Follow me on: https://github.com/yakky https://github.com/nephila @yakkys @[email protected] https://keybase.io/yakky

Slide 54

Slide 54 text

QUESTIONS? QUESTIONS?