-
Notifications
You must be signed in to change notification settings - Fork 29
Elixir loqui #3
Elixir loqui #3
Conversation
@vishnevskiy @jhgg ready for a review, it passes the client benchmark test with 75k rps |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quick feedback, will have more
@@ -0,0 +1,215 @@ | |||
defmodule Loqui.CowboyProtocol do | |||
@opcode_hello 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can remove these from this file
@opcode_error 9 | ||
|
||
@supported_versions [1] | ||
@supported_encodings MapSet.new(["erlpack"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should be passed in as options
end | ||
|
||
def handle_data(<<@opcode_hello :: uint8, flags :: uint8, version :: uint8, psize :: uint32, payload :: binary-size(psize), rest :: binary>>) do | ||
settings = parse_settings(payload) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If parsing fails an error should return so the consumer of the parser can close the socket. (aka not enough options)
|
||
def handler_init(%{transport: transport, req: req, handler: handler, handler_opts: handler_opts, env: env}=state) do | ||
case handler.loqui_init(transport, req, handler_opts) do | ||
{:ok, req2, ping_interval, handler_state} -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can call these req
since its Elixir not Erlang.
end | ||
end | ||
|
||
def socket_data(state, data) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be private?
@@ -0,0 +1,44 @@ | |||
defmodule Loqui.Messages do | |||
# TODO: how to share these opcodes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made an opcodes module that is just a macro then you can do use Loqui.Opcodes
@opcode_goaway 8 | ||
@opcode_error 9 | ||
|
||
# TODO: how to share these macros |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above but for types
nit: I deliberately made encoding and compressions a list - as in the py/go implementations the ordering matters. The idea is that the client can specify the encodings it prefers first, before the encodings it supports. (Also, it's a bit overkill to use a MapSet for something that contains 1-2 things at most usually) |
if setting == "" do | ||
MapSet.new() | ||
else | ||
MapSet.new(String.split(setting, ",")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should use lists, not sets for this because we need to preserve the order the clients send this data in.
defmodule Loqui.Types do | ||
defmacro __using__(_) do | ||
quote do | ||
@opcode_hello 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this repeated in 2 places still?
end | ||
|
||
defp parse_setting(setting) do | ||
setting = String.trim(setting) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need to trim? Can't you just match in the function.
handler.handle_request(request, handler_state) | ||
end | ||
defp handler_push(%{handler: handler, handler_state: handler_state}, request) do | ||
handler.handle_push(request, handler_state) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should be consistent either the prefix should be handle_
or loqui_
def handler_init(%{transport: transport, req: req, handler: handler, handler_opts: handler_opts, env: env}=state) do | ||
case handler.loqui_init(transport, req, handler_opts) do | ||
{:ok, req, handler_state, opts} -> | ||
state = %{state | req: req, handler_state: handler_state} |> set_opts(opts) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not %{state | req: req, handler_state: handler_state} |> set_opts(opts) |> loqui_handshake
defmodule Loqui.Messages do | ||
use Loqui.{Opcodes, Types} | ||
|
||
def make_hello_ack(flags, ping_interval, settings_payload) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is a module for generating these values I feel like make_
prefix is redundant.
|
||
@default_ping_interval 5_000 | ||
@supported_versions [1] | ||
@default_supported_encodings MapSet.new(["erlpack"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't think we should have these defaults. I am going to ask Jake to remove it from the Python one. This should be always passed in for the config.
alias Loqui.{Parser, Messages} | ||
require Logger | ||
|
||
@default_ping_interval 5_000 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems low.
!Enum.member?(@supported_versions, version) -> | ||
goaway(state, :unsupported_version) | ||
{:shutdown, :unsupported_version} | ||
common_encodings == [] -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enum.empty?
true -> | ||
flags = 0 | ||
encoding = List.first(common_encodings) | ||
settings_payload = "#{encoding}|" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
compressions not implemented yet?
end | ||
defp handle_request({:request, _flags, seq, request}, state) do | ||
request = decode(state, request) | ||
{response, handler_state} = handler_request(state, request) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
request
and push
should happen in a child process as they can do blocking operations. This library should handle having a pool of workers per connection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the easiest route to take is that Elixir application that houses this code should just have a Supervisor as its main child that is either poolboy or gen stage? You can just feed it a job, monitor it and make sure it either responds to you or crashes. The same pool can be used across all clients unlike how we did it in Go and Python.
Does ping interval do nothing so far? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good. Just need the pool of procs now.
ping_timeout_ref: nil | ||
|
||
def upgrade(req, env, handler, handler_opts) do | ||
{_, ref} = :lists.keyfind(:listener, 1, env) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't you just do env[:listener]
in Elixir?
supported_compressions: supported_compressions} | ||
end | ||
|
||
def encode(%{encoding: "erlpack"}, msg), do: :erlang.term_to_binary(msg) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These shouldn't exist. The user of the library should encode and decode the data before talking to this library
@@ -0,0 +1,24 @@ | |||
defmodule Loqui.Messages do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We didn't call these anything I think in the other code. But these are usually called "frames" in protocols I think?
Rest seems good. |
Just typespecs then we golden! |
Not sure if I did the typespecs right, but they look nice. |
end | ||
|
||
def handle_cast({:request, {m, f, a}, seq, from}, state) do | ||
response = apply(m, f, a) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this crashes does the caller know to send_error
for that seq?
I modeled this off of the websocket cowboy protocol.
Here's what the discord changes look like: https://github.com/hammerandchisel/discord/compare/feature/elixir_drpc?expand=1