Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Phoenix Channels - a Distributed PubSub and Presence Platform

Phoenix Channels - a Distributed PubSub and Presence Platform

Channels are a really exciting and powerful part of the Phoenix Framework. They allow us to easily add soft-realtime features to our applications. Channels are based on a simple idea - sending and receiving messages.

In this talk, we'll take a look under the hood and learn how to build incredibly powerful, event driven systems with Phoenix Channels. We'll also look at how Phoenix makes it easier to track presence in a distributed system.

Sonny Scroggin

July 20, 2016
Tweet

More Decks by Sonny Scroggin

Other Decks in Programming

Transcript

  1. What can I build with it? • Stateless (typical CRUD,

    REST APIs, etc). • Stateful (persistent connections, efficient, event-based) • Distributed Systems
  2. UserSocket defmodule Chat.Endpoint do use Phoenix.Endpoint, otp_app: :chat socket "/socket",

    Chat.UserSocket # Serve at "/" the given assets from "priv/static" directory plug Plug.Static, at: "/", from: :chat, only: ~w(css images js favicon.ico robots.txt) # Code reloading will only work if the :code_reloader key of # the :phoenix application is set to true in your config file. if code_reloading? do socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
  3. UserSocket defmodule Chat.UserSocket do use Phoenix.Socket channel "room:*", Chat.RoomChannel transport

    :websocket, Phoenix.Transports.WebSocket transport :longpoll, Phoenix.Transports.LongPoll def connect(_params, socket) do {:ok, socket} end def id(_socket), do: nil end
  4. UserSocket defmodule Chat.UserSocket do use Phoenix.Socket channel "room:*", Chat.RoomChannel transport

    :websocket, Phoenix.Transports.WebSocket transport :longpoll, Phoenix.Transports.LongPoll def connect(_params, socket) do {:ok, socket} end def id(_socket), do: nil end
  5. UserSocket defmodule Chat.UserSocket do use Phoenix.Socket channel "room:*", Chat.RoomChannel transport

    :websocket, Phoenix.Transports.WebSocket transport :longpoll, Phoenix.Transports.LongPoll def connect(_params, socket) do {:ok, socket} end def id(_socket), do: nil end
  6. UserSocket defmodule Chat.UserSocket do use Phoenix.Socket channel "room:*", Chat.RoomChannel transport

    :websocket, Phoenix.Transports.WebSocket transport :longpoll, Phoenix.Transports.LongPoll def connect(_params, socket) do {:ok, socket} end def id(_socket), do: nil end
  7. RoomChannel defmodule Chat.RoomChannel do use Phoenix.Channel def join("room:lobby", _msg, socket)

    do {:ok, socket} end def join("room:" <> subtopic, _msg, socket) do case RoomServer.member?(socket.assigns[:user]) do true -> {:ok, socket} false -> {:error, %{reason: "unauthorized"}} end end def handle_in("new:msg", msg, socket) do broadcast! socket, "new:msg", %{user: msg["user"], body: msg["body"]} {:noreply, socket} end end
  8. RoomChannel defmodule Chat.RoomChannel do use Phoenix.Channel def join("room:lobby", _msg, socket)

    do {:ok, socket} end def join("room:" <> subtopic, _msg, socket) do case RoomServer.member?(socket.assigns[:user]) do true -> {:ok, socket} false -> {:error, %{reason: "unauthorized"}} end end def handle_in("new:msg", msg, socket) do broadcast! socket, "new:msg", %{user: msg["user"], body: msg["body"]} {:noreply, socket} end end
  9. RoomChannel defmodule Chat.RoomChannel do use Phoenix.Channel def join("room:lobby", _msg, socket)

    do {:ok, socket} end def join("room:" <> subtopic, _msg, socket) do case RoomServer.member?(socket.assigns[:user]) do true -> {:ok, socket} false -> {:error, %{reason: "unauthorized"}} end end def handle_in("new:msg", msg, socket) do broadcast! socket, "new:msg", %{user: msg["user"], body: msg["body"]} {:noreply, socket} end end
  10. RoomChannel defmodule Chat.RoomChannel do use Phoenix.Channel def join("room:lobby", _msg, socket)

    do {:ok, socket} end def join("room:" <> subtopic, _msg, socket) do case RoomServer.member?(socket.assigns[:user]) do true -> {:ok, socket} false -> {:error, %{reason: "unauthorized"}} end end def handle_in("new:msg", msg, socket) do broadcast! socket, "new:msg", %{user: msg["user"], body: msg["body"]} {:noreply, socket} end end
  11. RoomChannel defmodule Chat.RoomChannel do use Phoenix.Channel def join("room:lobby", _msg, socket)

    do {:ok, socket} end def join("room:" <> subtopic, _msg, socket) do case RoomServer.member?(socket.assigns[:user]) do true -> {:ok, socket} false -> {:error, %{reason: "unauthorized"}} end end def handle_in("new:msg", msg, socket) do broadcast! socket, "new:msg", %{user: msg["user"], body: msg["body"]} {:noreply, socket} end end
  12. RoomChannel defmodule Chat.RoomChannel do use Phoenix.Channel def join("room:lobby", _msg, socket)

    do {:ok, socket} end def join("room:" <> subtopic, _msg, socket) do case RoomServer.member?(socket.assigns[:user]) do true -> {:ok, socket} false -> {:error, %{reason: "unauthorized"}} end end def handle_in("new:msg", msg, socket) do broadcast! socket, "new:msg", %{user: msg["user"], body: msg["body"]} {:noreply, socket} end end
  13. RoomChannel defmodule Chat.RoomChannel do use Phoenix.Channel def join("room:lobby", _msg, socket)

    do {:ok, socket} end def join("room:" <> subtopic, _msg, socket) do case RoomServer.member?(socket.assigns[:user]) do true -> {:ok, socket} false -> {:error, %{reason: "unauthorized"}} end end def handle_in("new:msg", msg, socket) do broadcast! socket, "new:msg", %{user: msg["user"], body: msg["body"]} {:noreply, socket} end end
  14. import {Socket} from "phoenix" const socket = new Socket("/socket", {

    params: { userToken: window.userToken } }) socket.connect() const chan = socket.channel("room:lobby", {}) chan.join().receive("ignore", () => console.log("auth error")) .receive("ok", () => console.log("join ok")) .after(10000, () => console.log("Connection interruption")) chan.onClose(e => console.log("channel closed", e)) chan.push("new:msg", {user: "scrogson", body: "Hallo!"}) chan.on("new:msg", msg => { $messages.append(this.messageTemplate(msg)) })
  15. import {Socket} from "phoenix" const socket = new Socket("/socket", {

    params: { userToken: window.userToken } }) socket.connect() const chan = socket.channel("room:lobby", {}) chan.join().receive("ignore", () => console.log("auth error")) .receive("ok", () => console.log("join ok")) .after(10000, () => console.log("Connection interruption")) chan.onClose(e => console.log("channel closed", e)) chan.push("new:msg", {user: "scrogson", body: "Hallo!"}) chan.on("new:msg", msg => { $messages.append(this.messageTemplate(msg)) })
  16. import {Socket} from "phoenix" const socket = new Socket("/socket", {

    params: { userToken: window.userToken } }) socket.connect() const chan = socket.channel("room:lobby", {}) chan.join().receive("ignore", () => console.log("auth error")) .receive("ok", () => console.log("join ok")) .after(10000, () => console.log("Connection interruption")) chan.onClose(e => console.log("channel closed", e)) chan.push("new:msg", {user: "scrogson", body: "Hallo!"}) chan.on("new:msg", msg => { $messages.append(this.messageTemplate(msg)) })
  17. import {Socket} from "phoenix" const socket = new Socket("/socket", {

    params: { userToken: window.userToken } }) socket.connect() const chan = socket.channel("room:lobby", {}) chan.join().receive("ignore", () => console.log("auth error")) .receive("ok", () => console.log("join ok")) .after(10000, () => console.log("Connection interruption")) chan.onClose(e => console.log("channel closed", e)) chan.push("new:msg", {user: "scrogson", body: "Hallo!"}) chan.on("new:msg", msg => { $messages.append(this.messageTemplate(msg)) })
  18. import {Socket} from "phoenix" const socket = new Socket("/socket", {

    params: { userToken: window.userToken } }) socket.connect() const chan = socket.channel("room:lobby", {}) chan.join().receive("ignore", () => console.log("auth error")) .receive("ok", () => console.log("join ok")) .after(10000, () => console.log("Connection interruption")) chan.onClose(e => console.log("channel closed", e)) chan.push("new:msg", {user: "scrogson", body: "Hallo!"}) chan.on("new:msg", msg => { $messages.append(this.messageTemplate(msg)) })
  19. import {Socket} from "phoenix" const socket = new Socket("/socket", {

    params: { userToken: window.userToken } }) socket.connect() const chan = socket.channel("room:lobby", {}) chan.join().receive("ignore", () => console.log("auth error")) .receive("ok", () => console.log("join ok")) .after(10000, () => console.log("Connection interruption")) chan.onClose(e => console.log("channel closed", e)) chan.push("new:msg", {user: "scrogson", body: "Hallo!"}) chan.on("new:msg", msg => { $messages.append(this.messageTemplate(msg)) })
  20. import {Socket} from "phoenix" const socket = new Socket("/socket", {

    params: { userToken: window.userToken } }) socket.connect() const chan = socket.channel("room:lobby", {}) chan.join().receive("ignore", () => console.log("auth error")) .receive("ok", () => console.log("join ok")) .after(10000, () => console.log("Connection interruption")) chan.onClose(e => console.log("channel closed", e)) chan.push("new:msg", {user: "scrogson", body: "Hallo!"}) chan.on("new:msg", msg => { $messages.append(this.messageTemplate(msg)) })
  21. import {Socket} from "phoenix" const socket = new Socket("/socket", {

    params: { userToken: window.userToken } }) socket.connect() const chan = socket.channel("room:lobby", {}) chan.join().receive("ignore", () => console.log("auth error")) .receive("ok", () => console.log("join ok")) .after(10000, () => console.log("Connection interruption")) chan.onClose(e => console.log("channel closed", e)) chan.push("new:msg", {user: "scrogson", body: "Hallo!"}) chan.on("new:msg", msg => { $messages.append(this.messageTemplate(msg)) })
  22. import {Socket} from "phoenix" const socket = new Socket("/socket", {

    params: { userToken: window.userToken } }) socket.connect() const chan = socket.channel("room:lobby", {}) chan.join().receive("ignore", () => console.log("auth error")) .receive("ok", () => console.log("join ok")) .after(10000, () => console.log("Connection interruption")) chan.onClose(e => console.log("channel closed", e)) chan.push("new:msg", {user: "scrogson", body: "Hallo!"}) chan.on("new:msg", msg => { $messages.append(this.messageTemplate(msg)) })
  23. NODE01 NODE02 JOE ROBERT MIKE CHRIS JOSÉ SONNY Agent Agent

    Presence.list(“room:lobby”) => [JOE, ROBERT, MIKE] Presence.list(“room:lobby”) => [CHRIS, JOSÉ, SONNY]
  24. NODE01 NODE02 JOE ROBERT MIKE CHRIS JOSÉ SONNY Redis Presence.list(“room:lobby”)

    => [JOE, ROBERT, MIKE, CHRIS, JOSÉ, SONNY] Presence.list(“room:lobby”) => [JOE, ROBERT, MIKE, CHRIS, JOSÉ, SONNY]
  25. NODE01 NODE02 JOE ROBERT MIKE CHRIS JOSÉ SONNY Redis Presence.list(“room:lobby”)

    => [JOE, ROBERT, MIKE, CHRIS, JOSÉ, SONNY] orphaned data
  26. CAP

  27. CRDTs are used to replicate data across multiple computers in

    a network, executing updates without the need for remote synchronization.
  28. NODE01 NODE02 NODE03 JOE ROBERT MIKE ROBERT MIKE ??? MIKE

    JOE ROBERT [JOE] [JOE] [JOE] should node02 add or discard Joe? slow message
  29. NODE02 NODE01 NODE03 {1, 0, 0} {1, 1, 0} {1,

    2, 3} {2, 2, 3} {0, 0, 1} {0, 0, 2} {0, 0, 3} A B D E C F G
  30. defmodule RoomChannel do use Phoenix.Channel def join("room:" <> id, _,

    socket) do send self(), :after_join {:ok, socket} end def handle_info(:after_join, socket) do id = socket.assigns.user_id push socket, "presence_state", Presence.list(socket) Presence.track(socket, id, %{status: "available"}) {:noreply, socket} end end
  31. defmodule RoomChannel do use Phoenix.Channel def join(“room:” <> id, _,

    socket) do send self(), :after_join {:ok, socket} end def handle_info(:after_join, socket) do id = socket.assigns.user_id push socket, "presence_state", Presence.list(socket) Presence.track(socket, id, %{status: “available"}) {:noreply, socket} end end
  32. import {Socket, Presence} from "phoenix" const socket = new Socket("/socket")

    socket.connect() const room = socket.channel(`room:${roomId}`) let presenceState = {} room.on("presence_state", state => { Presence.syncState(presenceState, state) }) room.on("presence_diff", diff => { Presence.syncDiff(presenceState, diff) }) console.log("users", Presence.list(presenceState))
  33. import {Socket, Presence} from "phoenix" const socket = new Socket("/socket")

    socket.connect() const room = socket.channel(`room:${roomId}`) let presenceState = {} room.on("presence_state", state => { Presence.syncState(presenceState, state) }) room.on("presence_diff", diff => { Presence.syncDiff(presenceState, diff) }) console.log("users", Presence.list(presenceState))
  34. import {Socket, Presence} from "phoenix" const socket = new Socket("/socket")

    socket.connect() const room = socket.channel(`room:${roomId}`) let presenceState = {} room.on("presence_state", state => { Presence.syncState(presenceState, state) }) room.on("presence_diff", diff => { Presence.syncDiff(presenceState, diff) }) console.log("users", Presence.list(presenceState))
  35. import {Socket, Presence} from "phoenix" const socket = new Socket("/socket")

    socket.connect() const room = socket.channel(`room:${roomId}`) let presenceState = {} room.on("presence_state", state => { Presence.syncState(presenceState, state) }) room.on("presence_diff", diff => { Presence.syncDiff(presenceState, diff) }) console.log("users", Presence.list(presenceState))
  36. import {Socket, Presence} from "phoenix" const socket = new Socket("/socket")

    socket.connect() const room = socket.channel(`room:${roomId}`) let presenceState = {} room.on("presence_state", state => { Presence.syncState(presenceState, state) }) room.on("presence_diff", diff => { Presence.syncDiff(presenceState, diff) }) console.log("users", Presence.list(presenceState))
  37. import {Socket, Presence} from "phoenix" const socket = new Socket("/socket")

    socket.connect() const room = socket.channel(`room:${roomId}`) let presenceState = {} room.on("presence_state", state => { Presence.syncState(presenceState, state) }) room.on("presence_diff", diff => { Presence.syncDiff(presenceState, diff) }) console.log("users", Presence.list(presenceState))
  38. import {Socket, Presence} from "phoenix" const socket = new Socket("/socket")

    socket.connect() const room = socket.channel(`room:${roomId}`) let presenceState = {} room.on("presence_state", state => { Presence.syncState(presenceState, state) }) room.on("presence_diff", diff => { Presence.syncDiff(presenceState, diff) }) console.log("users", Presence.list(presenceState))
  39. # pseudo code Service.track(pid, “email", %{ pid: pid, max_jobs: 100,

    work_load: 0 }) Service.all => %{“email” => [...]} Service.list("email") => [%{pid: pid, max_jobs: 100, work_load: 50}, ...)]