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 PyCon Italia Firenze - 20 Aprile

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 a few weeks old Channels 1 served for almost three years Will focus on 2 (2.1, really) (Check previous version of this talk)

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 ASYNCRONOUS ASYNCRONOUS 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

Slide 16

Slide 16 text

CONCEPTS CONCEPTS EVENTS EVENTS Triggered during a scope Consumed by consumers

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

CONCEPTS CONCEPTS GROUP GROUP Group consumer instances on channel layer

Slide 19

Slide 19 text

TIME HAS PASSED TIME HAS PASSED CHANNELS 1 → 2 CHANNELS 1 → 2

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

CHANNELS 1 → 2 CHANNELS 1 → 2 UPGRADING UPGRADING Major changes Channel-as-IPC only Stateful consumers Custom events architecture

Slide 22

Slide 22 text

CHANNELS 1 → 2 CHANNELS 1 → 2 UPGRADING UPGRADING Rewrite needed Routing Consumers (partial) Tests Check channels1 vs channels2 branches

Slide 23

Slide 23 text

DEMO TIME! DEMO TIME! Enough theory

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

DEMO TIME DEMO TIME FEATURES FEATURES Count active users Concurrency check

Slide 26

Slide 26 text

DEMO TIME DEMO TIME 0:00 / 0:48

Slide 27

Slide 27 text

DIVE IN DIVE IN Channel layers Routing Consumers

Slide 28

Slide 28 text

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

Slide 29

Slide 29 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 30

Slide 30 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 31

Slide 31 text

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

Slide 32

Slide 32 text

DIVE IN DIVE IN CONSUMERS CONSUMERS

Slide 33

Slide 33 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 34

Slide 34 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 35

Slide 35 text

CONSUMERS CONSUMERS COUNTING USERS COUNTING USERS

Slide 36

Slide 36 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 37

Slide 37 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 38

Slide 38 text

CONSUMERS CONSUMERS CONCURRENCY MONITOR CONCURRENCY MONITOR

Slide 39

Slide 39 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 40

Slide 40 text

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

Slide 41

Slide 41 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 42

Slide 42 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 43

Slide 43 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 44

Slide 44 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 45

Slide 45 text

FAQ FAQ Performance Deployments

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

P.S. P.S. http://bit.ly/channels1

Slide 49

Slide 49 text

P.P.S. P.P.S. We are hiring https://nephila.digital/jobs

Slide 50

Slide 50 text

QUESTIONS? QUESTIONS?