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.

06f8b41980eb4c577fa40c41d5030c19?s=128

Chris Keathley

January 08, 2016
Tweet

Transcript

  1. REAL-TIME APPLICATIONS IN PHOENIX @ChrisKeathley / keathley@carbonfive.com / keathley.io

  2. Chris Keathley @ChrisKeathley / keathley@carbonfive.com / keathley.io

  3. Lets talk about Realtime

  4. Rails + Faye

  5. Rails Faye

  6. Node + socket.io

  7. socket.io socket.io Redis

  8. Complexity

  9. Balance

  10. None
  11. 2 million connections per server

  12. 2 million connections per server 400 million users

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

  14. Erlang

  15. Heroku RabbitMQ Riak Facebook

  16. Elixir

  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
  18. Pattern Matching iex(1)> x = 3 3

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

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

    x 3 iex(3)> 3 = x 3
  21. Pattern Matching is an assertion

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

  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)>
  24. Immutability

  25. Processes iex(9)> pid = spawn fn -> 1 + 2

    end #PID<0.68.0> iex(10)> self() #PID<0.57.0>
  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>"
  27. Processes C# Thread: 4Mb

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

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

  30. OTP GenServer Supervisors GenFSM GenEvent Observer Dialyzer

  31. Let It Crash

  32. None
  33. ❤ Real-time

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

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

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

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

    Changesets
  38. Controllers defmodule Conflicted.PageController do use Conflicted.Web, :controller def index(conn, _params)

    do render conn, "index.html" end end
  39. Controllers <!DOCTYPE html> <html lang="en"> <head> <title>Conflicted</title> </head> <body> <%=

    render Conflicted.PageView, "index.html" %> <%= if Mix.env == :dev do %> <script src="http://localhost:4001/bundle.js"></script> <% else %> <script src="<%= static_path(@conn, "/js/bundle.js") %>"></script> <% end %> </body> </html>
  40. import { Socket } from 'phoenix-socket' let socket = new

    Socket("/socket") socket.connect()
  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
  42. ## Channels channel "tweets:*", Conflicted.TweetChannel

  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
  44. def join("tweets:stream", _message, socket) do

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

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

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

    channel Pattern matching on the channel name The client socket connection
  48. def join("tweets:stream", _message, socket) do

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

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

    socket} end Retrieves all “tweets” from the database
  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
  52. def join("tweets:stream", _message, socket) do tweets = Repo.all(Tweet) {:ok, tweets,

    socket} end
  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
  54. Lets build an app Controllers & Channels Models Supervisor Trees

    Changesets
  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
  56. DB Repo Query Data

  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)
  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
  59. Lets build an app Controllers & Channels Models Supervisor Trees

    Changesets
  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); })
  61. Channel infrastructure Phoenix Server Phoenix Server

  62. Phoenix Server Phoenix Server

  63. Phoenix Server Phoenix Server Clients Channels

  64. Phoenix Server Channel Channel

  65. Phoenix Server Repo Supervisor Channel Channel

  66. Phoenix Server Repo Supervisor Channel Channel

  67. Phoenix Server Repo Supervisor Channel

  68. Phoenix Server Repo Supervisor Channel Channel

  69. Phoenix Server Repo Supervisor Channel Channel TwitterClient

  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
  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
  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
  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
  74. channel.on("state", tweet => { store.dispatch(setState([tweet])) })

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

    Changesets
  76. export function setState(state) { return { type: SET_STATE, state }

    } export function likeTweet(id) { return { meta: {remote: true}, type: LIKE_TWEET, id } }
  77. export default channel => store => next => action =>

    { if (action.meta && action.meta.remote) channel.push('action', action) return next(action) }
  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 } }
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  89. So much more!

  90. Web Android Windows (C#) iOS

  91. Monitoring

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

  93. Thanks @Chris Keathley / keathley@carbonfive.com / keathley.io