+
Skip to content
This repository was archived by the owner on Nov 29, 2022. It is now read-only.

Elixir loqui #3

Merged
merged 33 commits into from
Dec 2, 2016
Merged

Elixir loqui #3

merged 33 commits into from
Dec 2, 2016

Conversation

jhowarth
Copy link
Contributor

@jhowarth jhowarth commented Nov 29, 2016

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

@jhowarth
Copy link
Contributor Author

@vishnevskiy @jhgg ready for a review, it passes the client benchmark test with 75k rps

Copy link
Contributor

@vishnevskiy vishnevskiy left a 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
Copy link
Contributor

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"])
Copy link
Contributor

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)
Copy link
Contributor

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} ->
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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

@jhgg
Copy link
Contributor

jhgg commented Nov 29, 2016

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, ","))
Copy link
Contributor

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
Copy link
Contributor

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)
Copy link
Contributor

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)
Copy link
Contributor

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)
Copy link
Contributor

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
Copy link
Contributor

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"])
Copy link
Contributor

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
Copy link
Contributor

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 == [] ->
Copy link
Contributor

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}|"
Copy link
Contributor

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)
Copy link
Contributor

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.

Copy link
Contributor

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.

@vishnevskiy
Copy link
Contributor

Does ping interval do nothing so far?

Copy link
Contributor

@vishnevskiy vishnevskiy left a 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)
Copy link
Contributor

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)
Copy link
Contributor

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
Copy link
Contributor

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?

@vishnevskiy
Copy link
Contributor

vishnevskiy commented Nov 30, 2016

  • For simplicity the pool should be started by this application (not a parent application), but expose pool configuration options on env.
  • Should just add typespecs for documentation.

Rest seems good.

@vishnevskiy
Copy link
Contributor

Just typespecs then we golden!

@jhowarth
Copy link
Contributor Author

jhowarth commented Dec 1, 2016

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)
Copy link
Contributor

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?

@jhowarth jhowarth merged commit 3cbf8e6 into master Dec 2, 2016
@jhowarth jhowarth deleted the ex branch December 2, 2016 01:10
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载