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

Real-Time applications in Phoenix

Real-Time applications in Phoenix

A talk about building real time applications in phoenix and elixir with examples for both the client side and server side.

Chris Keathley

January 08, 2016
Tweet

More Decks by Chris Keathley

Other Decks in Programming

Transcript

  1. REAL-TIME
    APPLICATIONS
    IN PHOENIX
    @ChrisKeathley / [email protected]five.com / keathley.io

    View Slide

  2. Chris Keathley
    @ChrisKeathley / [email protected]five.com / keathley.io

    View Slide

  3. Lets talk about
    Realtime

    View Slide

  4. Rails + Faye

    View Slide

  5. Rails
    Faye

    View Slide

  6. Node + socket.io

    View Slide

  7. socket.io
    socket.io
    Redis

    View Slide

  8. Complexity

    View Slide

  9. Balance

    View Slide

  10. View Slide

  11. 2 million connections per server

    View Slide

  12. 2 million connections per server
    400 million users

    View Slide

  13. 2 million connections per server
    400 million users
    30 Engineers

    View Slide

  14. Erlang

    View Slide

  15. Heroku
    RabbitMQ
    Riak
    Facebook

    View Slide

  16. Elixir

    View Slide

  17. Types
    iex> 1 # integer
    iex> 0x1F # integer
    iex> 1.0 # float
    iex> true # boolean
    iex> :atom # atom / symbol
    iex> "elixir" # string
    iex> [1, 2, 3] # list
    iex> {1, 2, 3} # tuple
    iex> %{:a => 1, 2 => :b} # map

    View Slide

  18. Pattern Matching
    iex(1)> x = 3
    3

    View Slide

  19. Pattern Matching
    iex(1)> x = 3
    3
    iex(2)> y = x
    3

    View Slide

  20. Pattern Matching
    iex(1)> x = 3
    3
    iex(2)> y = x
    3
    iex(3)> 3 = x
    3

    View Slide

  21. Pattern Matching is
    an assertion

    View Slide

  22. Pattern Matching
    iex(3)> 3 = x
    3

    View Slide

  23. Pattern Matching
    iex(4)> %{a: a, b: b} = %{a: "hello", b: "world"}
    %{a: "hello", b: "world"}
    iex(5)> a
    "hello"
    iex(6)> b
    "world"
    iex(7)>

    View Slide

  24. Immutability

    View Slide

  25. Processes
    iex(9)> pid = spawn fn -> 1 + 2 end
    #PID<0.68.0>
    iex(10)> self()
    #PID<0.57.0>

    View Slide

  26. Processes
    iex> parent = self()
    #PID<0.41.0>
    iex> spawn fn -> send(parent, {:hello, self()}) end
    #PID<0.48.0>
    iex> receive do
    ...> {:hello, pid} ->
    "Got hello from #{inspect pid}"
    ...> end
    "Got hello from #PID<0.48.0>"

    View Slide

  27. Processes
    C# Thread: 4Mb

    View Slide

  28. Processes
    C# Thread: 4Mb
    Java Thread: 256k

    View Slide

  29. Processes
    C# Thread: 4Mb
    Java Thread: 256k
    Erlang Process: 1k

    View Slide

  30. OTP
    GenServer
    Supervisors
    GenFSM
    GenEvent
    Observer
    Dialyzer

    View Slide

  31. Let It Crash

    View Slide

  32. View Slide


  33. Real-time

    View Slide

  34. http://www.phoenixframework.org/blog/the-road-to-2-million-
    websocket-connections

    View Slide

  35. Demo
    http://bit.ly/1OEWVQ2

    View Slide

  36. Lets build an app
    Controllers & Channels
    Models
    Supervisor Trees
    Changesets

    View Slide

  37. Lets build an app
    Controllers & Channels
    Models
    Supervisor Trees
    Changesets

    View Slide

  38. Controllers
    defmodule Conflicted.PageController do
    use Conflicted.Web, :controller
    def index(conn, _params) do
    render conn, "index.html"
    end
    end

    View Slide

  39. Controllers



    Conflicted


    <%= render Conflicted.PageView, "index.html" %>
    <%= if Mix.env == :dev do %>

    <% else %>
    ">
    <% end %>


    View Slide

  40. import { Socket } from 'phoenix-socket'
    let socket = new Socket("/socket")
    socket.connect()

    View Slide

  41. Sockets
    defmodule Conflicted.UserSocket do
    use Phoenix.Socket
    ## Channels
    channel "tweets:*", Conflicted.TweetChannel
    ## Transports
    transport :websocket, Phoenix.Transports.WebSocket
    transport :longpoll, Phoenix.Transports.LongPoll
    def connect(_params, socket) do
    {:ok, socket}
    end
    def id(_socket), do: nil
    end

    View Slide

  42. ## Channels
    channel "tweets:*", Conflicted.TweetChannel

    View Slide

  43. defmodule Conflicted.TweetChannel do
    use Phoenix.Channel
    def join("tweets:stream", _message, socket) do
    tweets = Conflicted.Repo.all(Conflicted.Tweet)
    {:ok, tweets, socket}
    end
    def join("tweets:" <> _private_room_id, _params, _socket) do
    {:error, %{reason: "unauthorized"}}
    end
    # …
    end

    View Slide

  44. def join("tweets:stream", _message, socket) do

    View Slide

  45. def join("tweets:stream", _message, socket) do
    When a client “joins” the channel

    View Slide

  46. def join("tweets:stream", _message, socket) do
    When a client “joins” the channel
    Pattern matching on the channel name

    View Slide

  47. def join("tweets:stream", _message, socket) do
    When a client “joins” the channel
    Pattern matching on the channel name
    The client socket connection

    View Slide

  48. def join("tweets:stream", _message, socket) do

    View Slide

  49. def join("tweets:stream", _message, socket) do
    tweets = Repo.all(Tweet)
    {:ok, tweets, socket}
    end

    View Slide

  50. def join("tweets:stream", _message, socket) do
    tweets = Repo.all(Tweet)
    {:ok, tweets, socket}
    end
    Retrieves all “tweets” from the database

    View Slide

  51. def join("tweets:stream", _message, socket) do
    tweets = Repo.all(Tweet)
    {:ok, tweets, socket}
    end
    Retrieves all “tweets” from the database
    Return a tuple including the socket

    View Slide

  52. def join("tweets:stream", _message, socket) do
    tweets = Repo.all(Tweet)
    {:ok, tweets, socket}
    end

    View Slide

  53. defmodule Conflicted.TweetChannel do
    use Phoenix.Channel
    def join("tweets:stream", _message, socket) do
    tweets = Repo.all(Tweet)
    {:ok, tweets, socket}
    end
    def join("tweets:" <> _private_room_id, _params, _socket) do
    {:error, %{reason: "unauthorized"}}
    end
    # …
    end
    Return an error if the client tries to join any other channel

    View Slide

  54. Lets build an app
    Controllers & Channels
    Models
    Supervisor Trees
    Changesets

    View Slide

  55. defmodule Conflicted.Tweet do
    use Conflicted.Web, :model
    schema "tweets" do
    field :author, :string
    field :content, :string
    field :source_url, :string
    field :image_url, :string
    field :likes, :integer, default: 0
    timestamps
    end
    end

    View Slide

  56. DB
    Repo
    Query
    Data

    View Slide

  57. query = from t in Tweet,
    select: [t.text, t.likes],
    where: [t.author == “@chriskeathley”],
    order_by: [desc: t.inserted_at],
    limit: 30
    tweets = Conflicted.Repo.all(query)

    View Slide

  58. defmodule Conflicted.TweetChannel do
    use Phoenix.Channel
    def join("tweets:stream", _message, socket) do
    tweets = Repo.all(Tweet)
    {:ok, tweets, socket}
    end
    def join("tweets:" <> _private_room_id, _params, _socket) do
    {:error, %{reason: "unauthorized"}}
    end
    # …
    end

    View Slide

  59. Lets build an app
    Controllers & Channels
    Models
    Supervisor Trees
    Changesets

    View Slide

  60. import { Socket } from 'phoenix-socket'
    let socket = new Socket("/socket")
    socket.connect()
    let channel = socket.channel("tweets:stream", {})
    channel.join()
    .receive("ok", resp => {
    store.dispatch(setState(resp))
    })
    .receive("error", resp => {
    console.log("Unable to join", resp);
    })

    View Slide

  61. Channel infrastructure
    Phoenix Server
    Phoenix Server

    View Slide

  62. Phoenix Server
    Phoenix Server

    View Slide

  63. Phoenix Server
    Phoenix Server
    Clients Channels

    View Slide

  64. Phoenix Server
    Channel Channel

    View Slide

  65. Phoenix Server Repo
    Supervisor
    Channel Channel

    View Slide

  66. Phoenix Server Repo
    Supervisor
    Channel Channel

    View Slide

  67. Phoenix Server Repo
    Supervisor
    Channel

    View Slide

  68. Phoenix Server Repo
    Supervisor
    Channel Channel

    View Slide

  69. Phoenix Server Repo
    Supervisor
    Channel Channel
    TwitterClient

    View Slide

  70. defmodule Conflicted do
    use Application
    def start(_type, _args) do
    import Supervisor.Spec, warn: false
    children = [
    supervisor(Conflicted.Endpoint, []),
    worker(Conflicted.Repo, []),
    worker(Task, [fn -> stream_task("elixirfriends") end])
    ]
    opts = [strategy: :one_for_one, name: Conflicted.Supervisor]
    Supervisor.start_link(children, opts)
    end
    defp stream_task(term) do
    Conflicted.TweetStreamer.stream(term)
    |> Enum.to_list
    end
    end

    View Slide

  71. defmodule Conflicted.TweetStreamer do
    alias Conflicted.Repo
    import Ecto.Query, only: [from: 2]
    def stream(search_term) do
    ExTwitter.stream_filter(track: search_term)
    |> Stream.filter(&has_images?/1)
    |> Stream.filter(&new_tweet?/1)
    |> Stream.map(&store_tweet/1)
    end
    defp store_tweet(raw_tweet) do
    tweet =
    raw_tweet
    |> new_tweet
    |> Repo.insert!
    |> IO.inspect
    Conflicted.Endpoint.broadcast!("tweets:stream", "state", tweet)
    end
    # …
    end

    View Slide

  72. def stream(search_term) do
    ExTwitter.stream_filter(track: search_term)
    |> Stream.filter(&has_images?/1)
    |> Stream.filter(&new_tweet?/1)
    |> Stream.map(&store_tweet/1)
    end

    View Slide

  73. def stream(search_term) do
    ExTwitter.stream_filter(track: search_term)
    |> Stream.filter(&has_images?/1)
    |> Stream.filter(&new_tweet?/1)
    |> Stream.map(&store_tweet/1)
    end
    defp store_tweet(raw_tweet) do
    tweet =
    raw_tweet
    |> new_tweet
    |> Repo.insert!
    |> IO.inspect
    Conflicted.Endpoint.broadcast!("tweets:stream", "state", tweet)
    end

    View Slide

  74. channel.on("state", tweet => {
    store.dispatch(setState([tweet]))
    })

    View Slide

  75. Lets build an app
    Controllers & Channels
    Models
    Supervisor Trees
    Changesets

    View Slide

  76. export function setState(state) {
    return {
    type: SET_STATE,
    state
    }
    }
    export function likeTweet(id) {
    return {
    meta: {remote: true},
    type: LIKE_TWEET,
    id
    }
    }

    View Slide

  77. export default channel => store => next => action => {
    if (action.meta && action.meta.remote)
    channel.push('action', action)
    return next(action)
    }

    View Slide

  78. import { Map } from 'immutable'
    import { SET_STATE, LIKE_TWEET } from '../actions'
    const setState = (state, newState) => {
    let tweets = newState.map((tweet) => [tweet.id, tweet])
    return state.merge(tweets)
    }
    const likeTweet = (state, id) => {
    return state.updateIn([id, 'likes'], val => val + 1)
    }
    export default function tweetsReducer(state=Map({}), action) {
    switch(action.type) {
    case SET_STATE:
    return setState(state, action.state)
    case LIKE_TWEET:
    return likeTweet(state, action.id)
    default:
    return state
    }
    }

    View Slide

  79. def handle_in("action", %{"type" => "LIKE_TWEET", "id" => id}, socket) do
    changeset =
    Tweet
    |> Repo.get!(id)
    |> Tweet.changeset(%{"likes" => tweet.likes+1})
    case Repo.update(changeset) do
    {:ok, tweet} ->
    broadcast!(socket, "state", tweet)
    {:error, changeset} ->
    IO.puts "SOMETHING WENT WRONG"
    IO.inspect changeset
    end
    {:noreply, socket}
    end

    View Slide

  80. Pattern match on the action
    def handle_in("action", %{"type" => "LIKE_TWEET", "id" => id}, socket) do
    changeset =
    Tweet
    |> Repo.get!(id)
    |> Tweet.changeset(%{"likes" => tweet.likes+1})
    case Repo.update(changeset) do
    {:ok, tweet} ->
    broadcast!(socket, "state", tweet)
    {:error, changeset} ->
    IO.puts "SOMETHING WENT WRONG"
    IO.inspect changeset
    end
    {:noreply, socket}
    end

    View Slide

  81. Increment the likes by 1
    Pattern match on the action
    def handle_in("action", %{"type" => "LIKE_TWEET", "id" => id}, socket) do
    changeset =
    Tweet
    |> Repo.get!(id)
    |> Tweet.changeset(%{"likes" => tweet.likes+1})
    case Repo.update(changeset) do
    {:ok, tweet} ->
    broadcast!(socket, "state", tweet)
    {:error, changeset} ->
    IO.puts "SOMETHING WENT WRONG"
    IO.inspect changeset
    end
    {:noreply, socket}
    end

    View Slide

  82. Increment the likes by 1
    Send a message back to all of the clients
    Pattern match on the action
    def handle_in("action", %{"type" => "LIKE_TWEET", "id" => id}, socket) do
    changeset =
    Tweet
    |> Repo.get!(id)
    |> Tweet.changeset(%{"likes" => tweet.likes+1})
    case Repo.update(changeset) do
    {:ok, tweet} ->
    broadcast!(socket, "state", tweet)
    {:error, changeset} ->
    IO.puts "SOMETHING WENT WRONG"
    IO.inspect changeset
    end
    {:noreply, socket}
    end

    View Slide

  83. defmodule Conflicted.Tweet do
    use Conflicted.Web, :model
    schema "tweets" do
    field :author, :string
    field :content, :string
    field :source_url, :string
    field :image_url, :string
    field :likes, :integer, default: 0
    timestamps
    end
    @required_fields ~w(author content source_url image_url)
    @optional_fields ~w(likes)
    def changeset(model, params \\ []) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> validate_number(:likes, greater_than_or_equal_to: 0)
    end
    end

    View Slide

  84. @required_fields ~w(author content source_url image_url)
    @optional_fields ~w(likes)
    def changeset(model, params \\ []) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> validate_number(:likes, greater_than_or_equal_to: 0)
    end
    Transform into changeset

    View Slide

  85. @required_fields ~w(author content source_url image_url)
    @optional_fields ~w(likes)
    def changeset(model, params \\ []) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> validate_number(:likes, greater_than_or_equal_to: 0)
    end
    Transform into changeset
    Provide some validation

    View Slide

  86. def handle_in("action", %{"type" => "LIKE_TWEET", "id" => id}, socket) do
    changeset =
    Tweet
    |> Repo.get!(id)
    |> Tweet.changeset(%{"likes" => tweet.likes+1})
    case Repo.update(changeset) do
    {:ok, tweet} ->
    broadcast!(socket, "state", tweet)
    {:error, changeset} ->
    IO.puts "SOMETHING WENT WRONG"
    IO.inspect changeset
    end
    {:noreply, socket}
    end

    View Slide

  87. Create a changeset
    def handle_in("action", %{"type" => "LIKE_TWEET", "id" => id}, socket) do
    changeset =
    Tweet
    |> Repo.get!(id)
    |> Tweet.changeset(%{"likes" => tweet.likes+1})
    case Repo.update(changeset) do
    {:ok, tweet} ->
    broadcast!(socket, "state", tweet)
    {:error, changeset} ->
    IO.puts "SOMETHING WENT WRONG"
    IO.inspect changeset
    end
    {:noreply, socket}
    end
    Execute the change

    View Slide

  88. Create a changeset
    def handle_in("action", %{"type" => "LIKE_TWEET", "id" => id}, socket) do
    changeset =
    Tweet
    |> Repo.get!(id)
    |> Tweet.changeset(%{"likes" => tweet.likes+1})
    case Repo.update(changeset) do
    {:ok, tweet} ->
    broadcast!(socket, "state", tweet)
    {:error, changeset} ->
    IO.puts "SOMETHING WENT WRONG"
    IO.inspect changeset
    end
    {:noreply, socket}
    end
    Execute the change
    Handle the response

    View Slide

  89. So much more!

    View Slide

  90. Web
    Android
    Windows (C#)
    iOS

    View Slide

  91. Monitoring

    View Slide

  92. Getting started
    • http://elixir-lang.org/
    • http://www.phoenixframework.org/docs/installation
    • http://www.phoenixframework.org/
    • https://github.com/keathley/conflicted

    View Slide

  93. Thanks
    @Chris Keathley / [email protected]five.com / keathley.io

    View Slide