Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Define behaviours for client and server adapters #200

Conversation

avillen
Copy link
Contributor

@avillen avillen commented Jul 7, 2021

Hi 👋

To give some context, I was trying to add tests to my integration using grpc. I found that you can set the adapter that you want on both client and server, and that makes it easier to test my code. I'm a big fan of Mox, I really think that simplifies the way to add tests, but we need a behaviour over we can create the mock.

This MR aims to create behaviours for both clients and servers, but for my personal use-case, with the client one would be enough. With this, an incomplete test example could be:

      expected_proto_response = ProtoSampe.new()

      grpc_channel = %GRPC.Channel{
        adapter: GRPCCLientMock
      }

      stream = %GRPC.Client.Stream{
        channel: grpc_channel,
        codec: GRPC.Codec.Proto,
        response_mod: ProtoSample
      }

      expect(GRPCCLientMock, :connect, fn _, _ -> {:ok, grpc_channel} end)
      expect(GRPCCLientMock, :send_request, fn _, _, _ -> stream end)
      expect(GRPCCLientMock, :recv_headers, fn _, _, _ -> {:ok, [], :fin} end)

      {:ok, data, _} =
        ProtoSample
        |> GRPC.Message.Protobuf.encode(expected_proto_response)
        |> GRPC.Message.to_data()

      expect(GRPCCLientMock, :recv_data_or_trailers, fn _, _, _ -> {:data, data} end)

      expect(GRPCCLientMock, :recv_data_or_trailers, fn _, _, _ ->
        {:trailers, %{"grpc-status" => "0"}}
      end)
      
     // Your request

I'm not a person with a lot of knowledge of both grpc and this library itself. This MR could be considered in WIP because and not pretty sure if I have properly defined the typespecs, or be closed without any problem if you consider.

Cheers and thanks for the library! Awesome job!

Copy link
Contributor

@polvalente polvalente left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started looking into this for issue #199.

Since there was already your pull request opened, perhaps it's best we focus on it instead!


@callback disconnect(channel) :: {:ok, channel} | {:error, any}

@type stream :: Stream.t()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use Stream.t() directly instead of adding this proxy type. Is there any reason for this path?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For clarity, I would also move all @type definitions to the top of the module

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH, the only reason about using this proxy type is because I did it to all the types. Also the idea is that in this way it could be used in the specs of the definitions. But I don't have a strong reason to do that. How do you consider using the proxy type or using it directly inline?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the in-line type is better for readability:

# Module defs
defmodule T do

@type t :: integer

end

defmodule M do
  @type custom :: T.t()
  @spec f(x :: T.t(), y :: custom) :: T.t()
  def f(x, y), do: x + y
end

# iex calls
iex(1)> t M  
@type custom() :: T.t()

iex(2)> h M.f

                                  def f(x, y)                                   

  @spec f(x :: T.t(), y :: custom()) :: T.t()

When exploring the API via IEx, the proxy type will force the user to call t module to get the typedefs and discover it is indeed a proxy

alias GRPC.Client.Stream
alias GRPC.Channel

@type opts :: any
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 it's better to use opts :: keyword() instead (perhaps even avoiding a custom type, inlining this wherever needed).

This would warrant some changes to the current implementation, but provides a better interface

@type message :: binary()
@callback send_request(stream, message, opts) :: stream

@type headers :: [{String.t(), String.t()}]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation would need to change, as I think it currently returns a string-map

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totally right

@callback send_request(stream, message, opts) :: stream

@type headers :: [{String.t(), String.t()}]
@type fin :: :fin | :nofin
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A typedoc would be great here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool! 😄

@@ -0,0 +1,27 @@
defmodule GRPC.ClientAdapter do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
defmodule GRPC.ClientAdapter do
defmodule GRPC.Client do

I think ClientAdapter is kinda redundant since the "adapter" part is actually what implements the behaviour specified here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem here is that, if we want to follow the same criteria with the GRPC.Server, we wont be able because there is already another file (lib/grpc/server.ex) with the same module name. WDYT about using GRPC.Adapter.Client and GRPC.Adapter.Server?

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 GRPC.Ports.Client/Server might be a better naming pattern (from the Ports and Adapters design pattern), and then we could have GRPC.Adapters.Client.Gun or something like that

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just my opinion, curently we have lib/grpc/client and lib/grpc/server which provides client code and server code separately. How about we move the adapter behaviour to inside those folders? Like:

# For server adapter behaviour.
lib/grpc/server/adapter.ex
# For client adapter behaviour.
lib/grpc/client/adapter.ex

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to prefer grouping all behaviours under a given namespace to make it easy to find all of them (so adapters.xxx), but I'm not set too hard on either option :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about grouping the code by context, something like this:

  • /client/
    • stream.ex
    • adapter.ex
    • adapter/
      • gun.ex
  • /server.ex
  • /server/
    • stream.ex
    • supervisor.ex
    • adapter.ex
    • adapter/
      • cowboy.ex
      • cowboy/
        • handler.ex

WDYT? You guys have a bigger picture and will work on the project more than me 😅 , so let me know which one suits better to you 😄

BTW, @polvalente if you are in a rush to merge this, please, go ahead and take the PR if you want!! In the end, I can only take this from time to time...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No rush at all! I think this discussion is somewhat important and relevant to this PR as well

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the suggested tree, but I would have the adapters directory instead of adapter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the suggested tree, but I would have the adapters directory instead of adapter

Nice! Agree with that! I'll push a commit with that ASAP 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like this? WDYT?

@@ -0,0 +1,27 @@
defmodule GRPC.ClientAdapter do
@moduledoc false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a moduledoc here is really important so that this module is discoverable in the docs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree!

@@ -0,0 +1,24 @@
defmodule GRPC.ServerAdapter do
@moduledoc false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically the same considerations as the client behaviour

@polvalente
Copy link
Contributor

Also, disclaimer: I'm not a maintainer for the library and have just added my 2 cents because I'm interested in having this library work in production

@sleipnir
Copy link
Collaborator

sleipnir commented Jul 2, 2022

Also, disclaimer: I'm not a maintainer for the library and have just added my 2 cents because I'm interested in having this library work in production

Hi Paulo, it's a pleasure to speak with you again.
I decided to comment since you gave me the cue.
Today this project doesn't seem to be being maintained by anyone, there are numerous requests for PRs and Issues open without any response. For that reason I created a fork of the project called Falco to try to continue, we are also trying to improve the performance of the library that has suffered a lot in the gRPCs benchmark tests.
If you want to take a look at the project https://github.com/eigr/falco

**Note, some of the PRs open here I already include in Falco. But we are not yet ready for production.I'm sharing this just for your knowledge.

@polvalente
Copy link
Contributor

Also, disclaimer: I'm not a maintainer for the library and have just added my 2 cents because I'm interested in having this library work in production

Hi Paulo, it's a pleasure to speak with you again.
I decided to comment since you gave me the cue.
Today this project doesn't seem to be being maintained by anyone, there are numerous requests for PRs and Issues open without any response. For that reason I created a fork of the project called Falco to try to continue, we are also trying to improve the performance of the library that has suffered a lot in the gRPCs benchmark tests.
If you want to take a look at the project https://github.com/eigr/falco

**Note, some of the PRs open here I already include in Falco. But we are not yet ready for production.I'm sharing this just for your knowledge.

@sleipnir that's awesome to know! For sure I'll take a look into it!

@polvalente
Copy link
Contributor

@avillen let me know if you want me to take over this PR

@avillen
Copy link
Contributor Author

avillen commented Jul 17, 2022

hi @polvalente!! thanks for your review 😄 I can take care of apply your suggestions, but where are you saying to apply those changes, on this repo or in this one https://github.com/eigr/falco?

@polvalente
Copy link
Contributor

hi @polvalente!! thanks for your review 😄 I can take care of apply your suggestions, but where are you saying to apply those changes, on this repo or in this one https://github.com/eigr/falco?

Here! I'm a maintainer for the repo now.
The idea is to concentrate the efforts on this repo.

I'm in touch with the people at falco and at another fork as well, so we can bring the best changes from the forks here

@avillen
Copy link
Contributor Author

avillen commented Jul 18, 2022

hi @polvalente!! thanks for your review 😄 I can take care of apply your suggestions, but where are you saying to apply those changes, on this repo or in this one https://github.com/eigr/falco?

Here! I'm a maintainer for the repo now. The idea is to concentrate the efforts on this repo.

I'm in touch with the people at falco and at another fork as well, so we can bring the best changes from the forks here

Nice to hear that! I'll take care of it along the week 😄

Copy link
Contributor Author

@avillen avillen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@polvalente I was reading your suggestions and I totally agree with them, I just have a few smaill questions

@@ -0,0 +1,27 @@
defmodule GRPC.ClientAdapter do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem here is that, if we want to follow the same criteria with the GRPC.Server, we wont be able because there is already another file (lib/grpc/server.ex) with the same module name. WDYT about using GRPC.Adapter.Client and GRPC.Adapter.Server?


@callback disconnect(channel) :: {:ok, channel} | {:error, any}

@type stream :: Stream.t()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH, the only reason about using this proxy type is because I did it to all the types. Also the idea is that in this way it could be used in the specs of the definitions. But I don't have a strong reason to do that. How do you consider using the proxy type or using it directly inline?

@type message :: binary()
@callback send_request(stream, message, opts) :: stream

@type headers :: [{String.t(), String.t()}]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totally right

@@ -0,0 +1,27 @@
defmodule GRPC.ClientAdapter do
@moduledoc false
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree!

@callback send_request(stream, message, opts) :: stream

@type headers :: [{String.t(), String.t()}]
@type fin :: :fin | :nofin
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool! 😄

…thub.com:avillen/grpc into define-behaviours-for-client-and-server-adapters
def start_endpoint(endpoint, port, opts \\ []) do
servers = endpoint.__meta__(:servers)
servers = GRPC.Server.servers_to_map(servers)
adapter = Keyword.get(opts, :adapter, GRPC.Adapter.Cowboy)
adapter = Keyword.get(opts, :adapter, GRPC.Server.Adapters.Cowboy)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you replace all of these Keyword.get(a, b, c) with Keyword.get(a, b) || c?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just the ones this PR touches is fine, though

Comment on lines 80 to 85
@type endpoint :: Adapter.endpoint()
@type server_port :: Adapter.server_port()
@type servers_list :: module | [module]
@type servers_map :: Adapter.servers_map()
@type stream :: Adapter.stream()
@type headers :: Adapter.headers()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's avoid proxy types

@type reply :: binary()
@callback send_reply(state, reply, opts) :: any()

@type headers :: map()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we rework headers into keyword() on a separate PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say yes! I can take care of it 😄

lib/grpc/stub.ex Outdated
@@ -98,6 +98,8 @@ defmodule GRPC.Stub do
end
end

@type channel :: GRPC.ClientAdapter.channel()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

proxy type

lib/grpc/stub.ex Outdated
@@ -125,7 +127,7 @@ defmodule GRPC.Stub do
* `:accepted_compressors` - tell servers accepted compressors, this can be used without `:compressor`
* `:headers` - headers to attach to each request
"""
@spec connect(String.t(), Keyword.t()) :: {:ok, GRPC.Channel.t()} | {:error, any}
@spec connect(String.t(), Keyword.t()) :: {:ok, channel} | {:error, any}
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 we should use GRPC.Channel.t() here as it was before

alias GRPC.Client.Stream
alias GRPC.Channel

@type opts :: any
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be a keyword?

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 currently the code passes maps as options. We need to rework both this and header maps to become keywords and lists of tuples instead

@avillen
Copy link
Contributor Author

avillen commented Jul 21, 2022

@polvalente @wingyplus I think I have address all your suggestions, but pretty sure I'm missing something 😅 Do you mind taking a look again? Those missing things I think that we can take care of it in another PR:
#200 (comment)
#200 (comment)


alias GRPC.Server.Adapters.Cowboy.Handler

@callback start(atom, %{String.t() => [module]}, non_neg_integer(), Keyword.t()) ::
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be atom() instead of just atom?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

module might be the same

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@callback start(atom, %{String.t() => [module]}, non_neg_integer(), Keyword.t()) ::
@callback start(atom(), %{String.t() => [module()]}, non_neg_integer(), Keyword.t()) ::

alias GRPC.Server.Adapters.Cowboy.Handler

@callback start(atom, %{String.t() => [module]}, non_neg_integer(), Keyword.t()) ::
{atom, any, non_neg_integer}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{atom, any, non_neg_integer}
{atom(), any(), non_neg_integer()}

@callback start(atom, %{String.t() => [module]}, non_neg_integer(), Keyword.t()) ::
{atom, any, non_neg_integer}

@callback stop(atom, %{String.t() => [module]}) :: :ok | {:error, :not_found}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@callback stop(atom, %{String.t() => [module]}) :: :ok | {:error, :not_found}
@callback stop(atom(), %{String.t() => [module()]}) :: :ok | {:error, :not_found}

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 we can standardize on having parens :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree and done (at least everywhere I see 😂 ), but I think that we should think about how we can add a validation about that 😅

@wingyplus
Copy link
Contributor

wingyplus commented Jul 21, 2022

Just a quick look from my mobile:

  • Found type declaration inconsistency ( () vs no () ).
    • I already add code suggestion, but not all places. You may take a look at it.
  • I think we should add @impl true to callback function in cowboy adapter and removing the typespec from it.

@avillen
Copy link
Contributor Author

avillen commented Jul 21, 2022

Just a quick look from my mobile:

  • Found type declaration inconsistency ( () vs no () ).

    • I already add code suggestion, but not all places. You may take a look at it.
  • I think we should add @impl true to callback function in cowboy adapter and removing the typespec from it.

@wingyplus I think it is done 😄

Copy link
Contributor

@polvalente polvalente left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

lib/grpc/client/adapter.ex Outdated Show resolved Hide resolved
lib/grpc/client/adapter.ex Outdated Show resolved Hide resolved
lib/grpc/client/adapter.ex Outdated Show resolved Hide resolved
lib/grpc/stub.ex Show resolved Hide resolved
@polvalente polvalente merged commit 736c878 into elixir-grpc:master Jul 21, 2022
@avillen avillen deleted the define-behaviours-for-client-and-server-adapters branch July 21, 2022 17:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants