Last minute changes

This commit is contained in:
Daniel Flanagan 2022-03-02 10:24:47 -06:00
parent 0d1068226a
commit ab0ede644a
Signed by: lytedev
GPG key ID: 5B2020A0F9921EF4
4 changed files with 222 additions and 54 deletions

26
Dockerfile Normal file
View file

@ -0,0 +1,26 @@
FROM elixir:1.13.2-slim
RUN apt-get update && \
apt-get -y --no-install-recommends install git curl ca-certificates && \
apt-get autoremove -y && \
apt-get clean && \
rm -rf /var/lib/apt/lists
# install livebook
RUN mix local.hex --force && mix local.rebar --force
RUN mix escript.install --force github livebook-dev/livebook
ENV LIVEBOOK_IP 0.0.0.0
ENV LIVEBOOK_HOME /app
ENV LIVEBOOK_PORT 5588
ENV LIVEBOOK_IFRAME_PORT 5589
EXPOSE 5588/tcp
EXPOSE 5589/tcp
WORKDIR /app
ADD . /app
RUN mix do deps.get, compile
CMD ["/root/.mix/escripts/livebook", "server", "--default-runtime", "mix", "--no-token"]

View file

@ -0,0 +1,101 @@
defmodule RanchTalk.SocketMessageDisplayer do
use Kino.JS
use Kino.JS.Live
@spec new() :: Kino.JS.Live.t()
def new() do
Kino.JS.Live.new(__MODULE__, [])
end
@impl true
def init(messages, ctx) do
{:ok, assign(ctx, messages: messages)}
end
@impl true
def handle_info({:form_message, %{data: data}}, ctx) do
data = Map.put(data, :date, DateTime.utc_now())
broadcast_event(ctx, "add_message", data)
new_messages = [data | ctx.assigns.messages]
{:noreply, assign(ctx, messages: new_messages)}
end
@impl true
def handle_cast({:subscribe_to_form, form}, ctx) do
subscribe_to_form(form)
{:noreply, ctx}
end
@impl true
def handle_connect(ctx) do
IO.inspect(ctx)
# subscribe_to_form(ctx.assigns.form)
{:ok, ctx.assigns, ctx}
end
defp subscribe_to_form(form) do
Kino.Control.subscribe(form, :form_message)
end
asset "main.js" do
"""
var cache = [
'',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
' '
]
function leftPad(str, len, ch) {
str = str + ''
len = len - str.length
if (len <= 0) return str
if (!ch && ch !== 0) ch = ' '
ch = ch + ''
if (ch === ' ' && len < 10) return cache[len] + str
var pad = ''
while (true) {
if (len & 1) pad += ch
len >>= 1
if (len) ch += ch
else break
}
return pad + str
}
function dateStr(date) {
console.log("before str", date)
const hours = leftPad(date.getHours().toString(), 2, '0')
const minutes = leftPad(date.getMinutes().toString(), 2, '0')
const seconds = leftPad(date.getSeconds().toString(), 2, '0')
return `${hours}:${minutes}:${seconds}`
}
export function init(ctx, state) {
const header = document.createElement("h1")
header.textContent = "Received Messages:"
ctx.root.appendChild(header)
const messages = document.createElement("ul")
messages.style.fontFamily = 'IosevkaLyte, monospace'
messages.style.fontSize = '16px'
ctx.root.appendChild(messages)
ctx.handleEvent("add_message", ({name, message, date}) => {
console.log("Raw Date", date)
const messageDate = new Date(date)
console.log("Message Date", messageDate)
const messageEl = document.createElement("li")
messageEl.textContent = `${dateStr(messageDate)} ${name}: ${message}`
messages.appendChild(messageEl)
})
}
"""
end
end

View file

@ -27,7 +27,7 @@ alias RanchTalk.TalkTimer
## Ranch Introduction
> Special thanks to Cody Poll for the excuse to waste a ton of time playing
> around with Livebook!
> around with Livebook! Source for this talk is available [here](https://git.lyte.dev/lytedev/ranch-talk)
From https://ninenines.eu/docs/en/ranch/2.1/guide/introduction/:
@ -71,7 +71,7 @@ loop look _very loosely_ like this now:
1. Open a socket
2. Check if any pending connections exist on the socket
3. If there are any pending connections, add them to our list of connections
- This is that "acceptor" part that Ranch takes care of for us
* This is that "acceptor" part that Ranch takes care of for us
4. For each active connection, check if the connection has any messages
5. If the connection has any messages, handle them
6. For each active connection, check if the connection is still active
@ -85,6 +85,8 @@ synchronously reaching out to a cache, then querying a database, or calling
another service. It would be stuck waiting for all of these operations while
new messages pour into your socket!
Let's look at a visual example... in Factorio!
### How does Ranch solve this?
Well, let's imagine _you_ are the poor, single-threaded program taking care of
@ -92,7 +94,7 @@ all this stuff. You're running around like mad from the OS socket, to the
connection list, shuffling messages all over the place, **and** you're
responsible for reading every single one, processing it, and responding.
Obviously, so modern web framework or socket library works this way for obvious
Obviously, no modern web framework or socket library works this way for obvious
reasons. You (or your machine) would be completely overwhelmed!
But this is where Ranch (and Erlang/OTP and Elixir) really shine.
@ -140,35 +142,39 @@ documentation to see how it works so we know better what to look for. Don't
worry, I'm not really going to make you read documentation yourself during
a talk, so I've summarized the important stuff we'll look at below:
- https://ninenines.eu/docs/en/ranch/2.1/guide/introduction/
- Just the stuff we've already talked about (minus all the boring socket
<!-- livebook:{"break_markdown":true} -->
* https://ninenines.eu/docs/en/ranch/2.1/guide/introduction/
* Just the stuff we've already talked about (minus all the boring socket
detail stuff)
- https://ninenines.eu/docs/en/ranch/2.1/guide/listeners/
- We start Ranch by adding the dependency and running
* https://ninenines.eu/docs/en/ranch/2.1/guide/listeners/
* We start Ranch by adding the dependency and running
[`:application.ensure_all_started(:ranch)`](https://ninenines.eu/docs/en/ranch/2.1/manual/)
- We can start a listener with
* We can start a listener with
[`:ranch.start_listener/5`](https://ninenines.eu/docs/en/ranch/2.1/manual/ranch.start_listener/)
- https://ninenines.eu/docs/en/ranch/2.1/guide/internals/
- Ranch is an OTP `Application` (named `:ranch`)
- It has a "top `Supervisor`" which supervises the `:ranch_server` process
* https://ninenines.eu/docs/en/ranch/2.1/guide/internals/
* Ranch is an OTP `Application` (named `:ranch`)
* It has a "top `Supervisor`" which supervises the `:ranch_server` process
_and_ any listeners
- Ranch uses a "custom `Supervisor`" for managing connections
- Listeners are grouped into the `:ranch_listener_sup` `Supervisor`
- Listeners consist of three kinds of processes:
- The listener `GenServer`
- A `Supervisor` that watches the acceptor processes
- The second argument to `:ranch/start_listener/5` indicates the number
* Ranch uses a "custom `Supervisor`" for managing connections
* Listeners are grouped into the `:ranch_listener_sup` `Supervisor`
* Listeners consist of three kinds of processes:
* The listener `GenServer`
* A `Supervisor` that watches the acceptor processes
* The second argument to `:ranch/start_listener/5` indicates the number
of processes that will be accepting new connections and we should be
careful choosing this number
- It defaults to `100`
- A `Supervisor` that watches the connection processes
- Each listener is registered with the `:ranch_server` `GenServer`
- All socket operations go through "transport handlers"
- These are simple callback modules (`@behaviour`s) for performing
* It defaults to `100`
* A `Supervisor` that watches the connection processes
* Each listener is registered with the `:ranch_server` `GenServer`
* All socket operations go through "transport handlers"
* These are simple callback modules (`@behaviour`s) for performing
operations on sockets
- Accepted connections are given to "the protocol handler" (just TCP for our
* Accepted connections are given to "the protocol handler" (just TCP for our
use case)
<!-- livebook:{"break_markdown":true} -->
Sweet! Armed with this knowledge, we should be able to find evidence of these
facts in our system _right now_. Let's do it!
@ -198,10 +204,10 @@ Now, if you selected the Livebook node and NOT the empty shell-of-a-node that
is the attached Mix project (you should switch to the correct one now!), you
will see that `:ranch_server` monitors a couple of `Supervisor`s:
- `:ranch_conns_sup`
- `:ranch_listener_sup`
- `:ranch_conns_sup`
- `:ranch_listener_sup`
* `:ranch_conns_sup`
* `:ranch_listener_sup`
* `:ranch_conns_sup`
* `:ranch_listener_sup`
Awesome! We can see the connection `Supervisor` and listener `Supervisor` for
port `5588` and likewise for port `5589`. The former for serving the page
@ -216,14 +222,11 @@ processes under `Monitors` hanging out waiting for connections. What gives?
Yeah, I dunno. Maybe somebody in the audience knows why they aren't monitored
(or at least why they don't show up here).
But if you go to [the Processes page](/dashboard/processes) and `Ctrl-F ":ranch_acceptor.loop"` you will see exactly 200 results.
But if you go to [the Processes page](/dashboard/processes) and search for `:ranch_acceptor.loop`, you will see exactly 200 results. Proof!
Ok, this is cool and all, and if we had time, we could look at this in the
Observer from pretty much any `iex` session like so:
Let's see how this plays out visually, yes, again, in Factorio! Then I'll show you how to use Ranch in the code below and we'll see if we can explore what it does.
```elixir
:observer.start()
```
<!-- livebook:{"break_markdown":true} -->
But we're all getting impatient to build our own Ranch. It won't have horses on
it, but it'll have something even better. TCP sockets!
@ -231,7 +234,7 @@ it, but it'll have something even better. TCP sockets!
## Building Your Own Ranch in 30 Seconds
My apologies to all the folks that built real ranches over much longer periods
of time and with far fewer TCP sockets to show for it.
of time and with far fewer TCP sockets to show for it. I hope this isn't too upsetting!
```elixir
Application.ensure_all_started(:ranch)
@ -241,23 +244,30 @@ Man, being able to take advantage of Open Source contributors' work is really
hard work. That was so easy! Now let's start accepting some TCP connections!
```elixir
socket_message_displayer = RanchTalk.SocketMessageDisplayer.new()
defmodule EchoHandler do
def start_link(ref, transport, opts) do
pid = spawn_link(__MODULE__, :init, [ref, transport, opts])
{:ok, pid}
end
def init(ref, transport, _opts \\ []) do
def init(ref, transport, opts) do
{:ok, socket} = :ranch.handshake(ref)
loop(socket, transport)
loop(socket, transport, ref, opts)
end
defp loop(socket, transport) do
defp loop(socket, transport, ref, opts) do
case transport.recv(socket, 0, 5000) do
{:ok, data} ->
IO.inspect(data)
displayer = Keyword.get(opts, :displayer)
transport.send(socket, data)
loop(socket, transport)
if displayer do
RanchTalk.SocketMessageDisplayer.add_message(displayer, data)
end
loop(socket, transport, ref, opts)
_ ->
:ok = transport.close(socket)
@ -265,9 +275,11 @@ defmodule EchoHandler do
end
end
:ranch.start_listener(:tcp_echo, :ranch_tcp, %{socket_opts: [port: 5555]}, EchoHandler, [])
:ranch.start_listener(:tcp_echo, :ranch_tcp, %{socket_opts: [port: 5555]}, EchoHandler,
displayer: socket_message_displayer
)
# Ranch Complete
socket_message_displayer
```
Ooh, _now_ if we look in our dashboard (at the non-Livebook node) we can see
@ -279,17 +291,35 @@ to it and see if it really does echo back to us! You can use `nc` (netcat),
`telnet`, or we can use Erlang's `:gen_tcp` like so:
```elixir
{:ok, socket} = :gen_tcp.connect({127, 0, 0, 1}, 5555, [:binary, active: true])
```
defmodule MyRanchClient do
def send_message(message) do
{:ok, socket} = :gen_tcp.connect({127, 0, 0, 1}, 5555, [:binary, active: true])
See how `:gen_tcp` returns `{:ok, #Port<...>}`? A `Port` is a special
Erlang/OTP-ism we can learn about another time. For now, should have got us
a connection! Let's send something.
:gen_tcp.send(socket, [
to_string(DateTime.utc_now()),
" ",
inspect(socket),
": ",
message
])
**NOTE**: If you don't hurry and send the message, the TCP socket will be closed due to your inactivity. Better act fast!
:gen_tcp.close(socket)
end
end
```elixir
:gen_tcp.send(socket, "Hello, socket! " <> to_string(DateTime.utc_now()))
form =
Kino.Control.form(
[
name: Kino.Input.text("Name", default: "Anonymous"),
message: Kino.Input.text("Message", default: "")
],
submit: "Send Message",
reset_on_submit: [:message]
)
Kino.JS.Live.cast(socket_message_displayer, {:subscribe_to_form, form})
form
```
And if we got `:ok`, this `Process` should have a message in its

View file

@ -11,18 +11,29 @@ Thanks to [Divvy][divvy] for inviting me to give this talk.
# Usage
Install and run a local Livebook in `attached` mode and automatically grab my
code:
```bash
asdf install
mix escript.install github livebook-dev/livebook
git clone https://git.lyte.dev/lytedev/ranch-talk.git
cd ranch-talk
mix do deps.get, compile
```
Install and run a local Livebook in `attached` mode and automatically grab my
code:
```fish
env LIVEBOOK_PORT=5588 LIVEBOOK_IFRAME_PORT=5589 \
livebook server --default-runtime mix \
"$(pwd)/ranch-talk.livemd"
LIVEBOOK_HOME=(pwd) LIVEBOOK_IP=0.0.0.0 \
livebook server --default-runtime mix --no-token
```
Or if you're gonna share this with everybody, it's probably safer to run it
inside of Docker:
```bash
docker build . --tag ranch-talk
docker run -it --rm -p 5588:5588 -p 5589:5589 ranch-talk
```
Enjoy!