feat: polymorphic helper for request body streaming#477
feat: polymorphic helper for request body streaming#477tank-bohr wants to merge 2 commits intoelixir-mint:mainfrom
Conversation
|
@ericmj Could you please take a look |
|
What if you make @type Mint.HTTP.t() :: Mint.HTTP1.t() | Mint.HTTP2.t()so that the "top-level" type is not opaque, but the inner types are? |
@whatyouhide That's exactly what my PR does. It un-opaques (exposes) the Mint.HTTP.t(). This will help to solve the dialyzer issue in the existing code (like k8s). Because existing code has to know about internals. And |
|
the original example with the get_send_window will look like defmodule MintDialyzerRepro do
def run(method, %URI{scheme: scheme} = uri, headers, body, opts)
when scheme in ~w[http https] do
scheme = String.to_existing_atom(scheme)
port = uri.port || URI.default_port(uri.scheme)
path = URI.to_string(%URI{uri | scheme: nil, host: nil, authority: nil})
with {:ok, conn} <- Mint.HTTP.connect(scheme, uri.host, port, opts) do
stream(conn, method, path, headers, body)
end
end
defp stream(conn, method, path, headers, body) do
with {:ok, conn, ref} <- Mint.HTTP.request(conn, method, path, headers, :stream) do
stream_body(conn, ref, body)
end
end
defp stream_body(conn, ref, "") do
Mint.HTTP.stream_request_body(conn, ref, :eof)
end
defp stream_body(conn, ref, body) do
chunk_size = Mint.HTTP.next_body_chunk_size(conn, ref, body)
<<chunk::binary-size(chunk_size), rest::binary>> = body
with {:ok, conn} <- Mint.HTTP.stream_request_body(conn, ref, chunk) do
stream_body(conn, ref, rest)
end
end
endAs you see no need for using the HTTP2 module directly. No need to detect protocol. The Mint.HTTP module dispatches to the correct implementation itself. With this change we can keep |
| end | ||
|
|
||
| @impl true | ||
| def get_send_window(%__MODULE__{streaming_request: %{ref: ref}, sndbuf: sndbuf}, ref), |
There was a problem hiding this comment.
This is semantically very different to what what the HTTP2 module does. There is no concept of send windows in HTTP/1 so we should not expose it on this module.
There was a problem hiding this comment.
I'm open for suggestions about naming. It was hard to come up with a good name which will reflect the meaning of the concept for both http1 and http2. But streaming exists, and it works for both http1 and http2. There is a number that plays the role of the http2 send window in the http1 streaming. Usually the arbitrary number is used. But I thought that the good default could be the biggest number possible in the current TCP configuration. Conceptually it's the same as HTTP2 does, it's just provided by the underlying protocol
There was a problem hiding this comment.
sndbuf serves a different purpose to H2 window size and has different behavior if you exceed it, blocking vs erroring. sndbuf can be relevant for H2 also, the TCP send buffer can be smaller than the window size, but I don't know how likely that is to happen.
If the purpose is to ensure you never block in gen_tcp.send then you should probably check sndbuf for H2 also or set send_timeout on the socket.
If we don't currently support these options then please send a separate PR.
There was a problem hiding this comment.
sndbuf in this context is not related to h2 at all. The main goal is to unify the streaming API. For streaming you need the chunk size. For http1.1 you need to get this number somehow. sndbuf is just the biggest reasonable chunk size for http1.1 streaming. At least that's how I understand the protocol. Naming is bad and to be discussed. But the idea of giving a universal API for streaming is still valid I believe
There was a problem hiding this comment.
If sndbuf is required for streaming H1 it is also required for H2. sndbuf is part of gen_tcp and both H1 and H2 use TCP.
The H2 send window is a contract with the receiving server we must obey, which is why Mint errors if you exceed it. sndbuf on the other hand is the size of the senders TCP send buffer, it only relates to the sender, it's not a contract with anyone. You would want to check it or tweak it for performance reasons.
Send windows as a concept does not exist for H1 so it's impossible to unify the APIs. The function should only exist on the HTTP2 module.
| end | ||
|
|
||
| @impl true | ||
| def get_send_window(%__MODULE__{} = conn, ref), |
There was a problem hiding this comment.
get_window_size/2 is already public, I don't think we need a function that only calls min on top of that.
There was a problem hiding this comment.
If it's simple it doesn't mean it's useless. If the vast majority of the code that uses streaming is written this way IMHO it makes sense to implement it in the library. Like a direction, like a guideline. Like it's most likely what you need. And if you need something special use the lower level API which is also available. And people need chunk size for streaming most of the time
There was a problem hiding this comment.
I would rather add a comment that describes the min pattern or find a better name for get_send_window, it doesn't match the API for get_window_size:
- Why does one have have _size in the name and not the other?
- Why does one have "send" in the name but not the other, they are both send windows?
- Just from the names I can't say what the difference is between get_send_window and get_window_size, I would have to read the docs.
I can't come up with a really good name, maybe "possible_send_size"?
There was a problem hiding this comment.
Name get_send_window is awful, I agree. possible_send_size sounds better to me. Maybe possible_chunk_size or streaming_chunk_size. Idk either
| def get_send_window(conn, ref), do: conn_apply(conn, :get_send_window, [conn, ref]) | ||
|
|
||
| @spec next_body_chunk_size(t(), term(), binary()) :: non_neg_integer() | ||
| def next_body_chunk_size(conn, ref, body), do: min(get_send_window(conn, ref), byte_size(body)) |
There was a problem hiding this comment.
I think this function is unnecessary.
Co-authored-by: Eric Meadows-Jönsson <eric.meadows.jonsson@gmail.com>
Hey guys. Let me explain the problem I faced with. Below is the typical code for the sreaming body request
This code generates dialyzer warnings
It was discussed in the #380 and was left with a dirty workaround.
And people are using the workaround. And on the OTP-28 such trick in the k8s library leads to the infinite hanging during the PLT building phase.
But it's a legit issue. And it's totally worth to be fixed.
Opaqueness
Since the caller have to know the exact type for streaming, the
Mint.HTTP.t()cannot be opaque. And the caller cannot know the exact type in advance either, because it's not their decision, but the server's decision.Missing API for streaming
Another consequence of this code and dialyzer warning is the request body streaming API is not sufficient for using it without knowing the underlying protocol. But it's kinda designed to be such. To close the gap the current PR is introducing the missing piece - an ability to get the chunk size for the streaming.