Skip to content

Commit 8eb787d

Browse files
committed
Add Mint.HTTP2.set_window_size/3
Advertises a larger HTTP/2 receive window (connection-level or per-stream) by sending a WINDOW_UPDATE frame. Needed because RFC 7540 makes the connection-level initial window tunable only via WINDOW_UPDATE — not SETTINGS — leaving the spec default of 64 KB as the only reachable value without an API like this. In hex's `mix deps.get` — many parallel multi-MB tarball downloads sharing one HTTP/2 connection — raising the connection window from 64 KB to 8 MB via this function drops 10 runs from 32.7s to 29.2s (10.8%), matching their HTTP/1 pool. Deliberately asymmetric with get_window_size/2 (which returns the client *send* window). Docstrings on both carry warning callouts spelling out send-vs-receive so callers don't assume they round-trip. Target is :connection or {:request, ref}; grow-only (shrink attempts return {:error, conn, %HTTPError{reason: :window_size_too_small}}); new_size validated against 1..2^31-1. Tracks the advertised peak on new receive_window_size fields on the connection and stream.
1 parent 0bfcc86 commit 8eb787d

2 files changed

Lines changed: 257 additions & 16 deletions

File tree

lib/mint/http2.ex

Lines changed: 166 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,16 @@ defmodule Mint.HTTP2 do
176176

177177
# Fields of the connection.
178178
buffer: "",
179+
# `window_size` is the client *send* window for the connection — how
180+
# much request-body data we're allowed to send to the server before it
181+
# refills the window with a WINDOW_UPDATE frame.
179182
window_size: @default_window_size,
183+
# `receive_window_size` is the client *receive* window for the
184+
# connection — the peak size we've advertised to the server via
185+
# `WINDOW_UPDATE` frames on stream 0 (or the spec default of 65_535
186+
# if we've never sent one). Mint auto-refills to maintain this peak
187+
# as DATA frames arrive.
188+
receive_window_size: @default_window_size,
180189
encode_table: HPAX.new(4096),
181190
decode_table: HPAX.new(4096),
182191

@@ -729,11 +738,21 @@ defmodule Mint.HTTP2 do
729738
end
730739

731740
@doc """
732-
Returns the window size of the connection or of a single request.
741+
Returns the client **send** window size for the connection or a request.
733742
734-
This function is HTTP/2 specific. It returns the window size of
735-
either the connection if `connection_or_request` is `:connection` or of a single
736-
request if `connection_or_request` is `{:request, request_ref}`.
743+
> #### Send vs receive windows {: .warning}
744+
>
745+
> This function returns the *send* window — how much body data this client
746+
> is still permitted to send to the server before being throttled. It is
747+
> decremented by `request/5` and `stream_request_body/3` and refilled by
748+
> the server, which `stream/2` handles transparently.
749+
>
750+
> It does **not** return the client *receive* window (how much the server
751+
> is permitted to send us). To influence that, use `set_window_size/3`.
752+
753+
This function is HTTP/2 specific. It returns the send window of either the
754+
connection if `connection_or_request` is `:connection` or of a single request
755+
if `connection_or_request` is `{:request, request_ref}`.
737756
738757
Use this function to check the window size of the connection before sending a
739758
full request. Also use this function to check the window size of both the
@@ -744,21 +763,23 @@ defmodule Mint.HTTP2 do
744763
745764
## HTTP/2 Flow Control
746765
747-
In HTTP/2, flow control is implemented through a
748-
window size. When the client sends data to the server, the window size is decreased
749-
and the server needs to "refill" it on the client side. You don't need to take care of
750-
the refilling of the client window as it happens behind the scenes in `stream/2`.
766+
In HTTP/2, flow control is implemented through a window size. When the client
767+
sends data to the server, the window size is decreased and the server needs
768+
to "refill" it on the client side, which `stream/2` handles transparently.
769+
Symmetrically, the server's outbound flow toward the client is bounded by a
770+
receive window the client advertises and refills — see `set_window_size/3`.
751771
752-
A window size is kept for the entire connection and all requests affect this window
753-
size. A window size is also kept per request.
772+
A window size is kept for the entire connection and all requests affect this
773+
window size. A window size is also kept per request.
754774
755-
The only thing that affects the window size is the body of a request, regardless of
756-
if it's a full request sent with `request/5` or body chunks sent through
757-
`stream_request_body/3`. That means that if we make a request with a body that is
758-
five bytes long, like `"hello"`, the window size of the connection and the window size
759-
of that particular request will decrease by five bytes.
775+
The only thing that affects the send window size is the body of a request,
776+
regardless of whether it's a full request sent with `request/5` or body chunks
777+
sent through `stream_request_body/3`. That means that if we make a request with
778+
a body that is five bytes long, like `"hello"`, the send window size of the
779+
connection and the send window size of that particular request will decrease
780+
by five bytes.
760781
761-
If we use all the window size before the server refills it, functions like
782+
If we use all the send window size before the server refills it, functions like
762783
`request/5` will return an error.
763784
764785
## Examples
@@ -797,6 +818,118 @@ defmodule Mint.HTTP2 do
797818
end
798819
end
799820

821+
@doc """
822+
Advertises a larger client **receive** window to the server.
823+
824+
> #### Receive vs send windows {: .warning}
825+
>
826+
> This function sets the *receive* window — the peak amount of body data
827+
> the server is permitted to send us before being throttled. It does
828+
> **not** set the *send* window (how much body data we're permitted to
829+
> send to the server) — the server controls that. See `get_window_size/2`
830+
> for the send window.
831+
832+
Without calling this, `stream/2` refills the receive window in small
833+
increments as response body data is consumed. Each refill costs a
834+
round-trip before the server can send more, so bulk throughput is capped
835+
at roughly `window / RTT`; on higher-latency links the default 64 KB
836+
window makes that cap well below the link bandwidth. Raising the window
837+
removes those pauses and is the main HTTP/2 tuning knob for bulk or
838+
highly parallel downloads.
839+
840+
Mint exposes the per-stream initial window as the `:initial_window_size`
841+
client setting passed to `connect/4`, but there is no connection-level
842+
equivalent — use this function for the connection window, and for any
843+
per-stream adjustment after a request has started.
844+
845+
`connection_or_request` is `:connection` for the whole connection or
846+
`{:request, request_ref}` for a single request. `new_size` must be in
847+
`1..2_147_483_647`. Windows can only grow: `new_size` smaller than the
848+
current receive window returns
849+
`{:error, conn, %Mint.HTTPError{reason: :window_size_too_small}}`, and
850+
`new_size` equal to the current window is a no-op.
851+
852+
For more information on flow control and window sizes in HTTP/2, see the
853+
section below.
854+
855+
## HTTP/2 Flow Control
856+
857+
See `get_window_size/2` for a description of the client *send* window.
858+
The client *receive* window is the symmetric bound on the server's
859+
outbound flow: it starts at 64 KB for the connection and for each new
860+
request, is decremented by response body bytes, and is refilled by
861+
`stream/2` as the body is consumed. A window size is kept for the entire
862+
connection and all responses affect this window size; a window size is
863+
also kept per request.
864+
865+
This function raises the *advertised* receive window — the peak the
866+
server is allowed to fill before pausing. It does not pre-allocate any
867+
buffers; it only permits the server to send further ahead of the
868+
client's reads.
869+
870+
## Examples
871+
872+
Bump the connection-level receive window right after connect so the server
873+
can stream multi-MB bodies without flow-control pauses:
874+
875+
{:ok, conn} = Mint.HTTP2.connect(:https, host, 443)
876+
{:ok, conn} = Mint.HTTP2.set_window_size(conn, :connection, 8_000_000)
877+
878+
Give one specific request a bigger window than the per-stream default:
879+
880+
{:ok, conn, ref} = Mint.HTTP2.request(conn, "GET", "/huge", [], nil)
881+
{:ok, conn} = Mint.HTTP2.set_window_size(conn, {:request, ref}, 16_000_000)
882+
883+
"""
884+
@spec set_window_size(t(), :connection | {:request, Types.request_ref()}, pos_integer()) ::
885+
{:ok, t()} | {:error, t(), Types.error()}
886+
def set_window_size(conn, connection_or_request, new_size)
887+
888+
def set_window_size(%__MODULE__{} = _conn, _target, new_size)
889+
when not (is_integer(new_size) and new_size >= 1 and new_size <= @max_window_size) do
890+
raise ArgumentError,
891+
"new window size must be an integer in 1..#{@max_window_size}, got: #{inspect(new_size)}"
892+
end
893+
894+
def set_window_size(%__MODULE__{} = conn, :connection, new_size) do
895+
do_set_window_size(conn, 0, conn.receive_window_size, new_size, fn conn, size ->
896+
put_in(conn.receive_window_size, size)
897+
end)
898+
catch
899+
:throw, {:mint, conn, error} -> {:error, conn, error}
900+
end
901+
902+
def set_window_size(%__MODULE__{} = conn, {:request, request_ref}, new_size) do
903+
case Map.fetch(conn.ref_to_stream_id, request_ref) do
904+
{:ok, stream_id} ->
905+
current = conn.streams[stream_id].receive_window_size
906+
907+
do_set_window_size(conn, stream_id, current, new_size, fn conn, size ->
908+
put_in(conn.streams[stream_id].receive_window_size, size)
909+
end)
910+
911+
:error ->
912+
{:error, conn, wrap_error({:unknown_request_to_stream, request_ref})}
913+
end
914+
catch
915+
:throw, {:mint, conn, error} -> {:error, conn, error}
916+
end
917+
918+
defp do_set_window_size(conn, _stream_id, current, new_size, _update) when new_size == current do
919+
{:ok, conn}
920+
end
921+
922+
defp do_set_window_size(conn, _stream_id, current, new_size, _update) when new_size < current do
923+
{:error, conn, wrap_error({:window_size_too_small, current, new_size})}
924+
end
925+
926+
defp do_set_window_size(conn, stream_id, current, new_size, update) do
927+
increment = new_size - current
928+
frame = window_update(stream_id: stream_id, window_size_increment: increment)
929+
conn = send!(conn, Frame.encode(frame))
930+
{:ok, update.(conn, new_size)}
931+
end
932+
800933
@doc """
801934
See `Mint.HTTP.stream/2`.
802935
"""
@@ -1083,7 +1216,15 @@ defmodule Mint.HTTP2 do
10831216
id: conn.next_stream_id,
10841217
ref: make_ref(),
10851218
state: :idle,
1219+
# Client send window — decremented as we send body bytes, refilled
1220+
# by incoming WINDOW_UPDATE frames from the server. Bounded initially
1221+
# by the server's SETTINGS_INITIAL_WINDOW_SIZE.
10861222
window_size: conn.server_settings.initial_window_size,
1223+
# Client receive window — the peak we've advertised to the server
1224+
# for this stream. Starts at whatever we told the server via our
1225+
# SETTINGS_INITIAL_WINDOW_SIZE; can be bumped per-stream with
1226+
# `set_window_size/3`.
1227+
receive_window_size: conn.client_settings.initial_window_size,
10871228
received_first_headers?: false
10881229
}
10891230

@@ -2223,6 +2364,15 @@ defmodule Mint.HTTP2 do
22232364
"can't stream chunk of data because the request is unknown"
22242365
end
22252366

2367+
def format_error({:unknown_request_to_stream, ref}) do
2368+
"request with reference #{inspect(ref)} was not found"
2369+
end
2370+
2371+
def format_error({:window_size_too_small, current, new_size}) do
2372+
"set_window_size/3 can only grow a window; new size #{new_size} is " <>
2373+
"smaller than the current size #{current}"
2374+
end
2375+
22262376
def format_error(:request_is_not_streaming) do
22272377
"can't send more data on this request since it's not streaming"
22282378
end

test/mint/http2/conn_test.exs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,97 @@ defmodule Mint.HTTP2Test do
131131
assert_http2_error error, {:protocol_error, "received invalid frame ping during handshake"}
132132
refute HTTP2.open?(conn)
133133
end
134+
135+
end
136+
137+
describe "set_window_size/3" do
138+
test "bumps the connection-level receive window by sending WINDOW_UPDATE on stream 0",
139+
%{conn: conn} do
140+
assert HTTP2.get_window_size(conn, :connection) == 65_535
141+
assert conn.receive_window_size == 65_535
142+
143+
assert {:ok, conn} = HTTP2.set_window_size(conn, :connection, 1_000_000)
144+
145+
assert conn.receive_window_size == 1_000_000
146+
147+
assert_recv_frames [
148+
window_update(stream_id: 0, window_size_increment: 934_465)
149+
]
150+
end
151+
152+
test "bumps a per-stream receive window by sending WINDOW_UPDATE on that stream",
153+
%{conn: conn} do
154+
{conn, ref} = open_request(conn)
155+
assert_recv_frames [headers(stream_id: stream_id)]
156+
157+
current = conn.streams[stream_id].receive_window_size
158+
159+
assert {:ok, conn} = HTTP2.set_window_size(conn, {:request, ref}, current + 10_000)
160+
161+
assert conn.streams[stream_id].receive_window_size == current + 10_000
162+
163+
assert_recv_frames [
164+
window_update(stream_id: ^stream_id, window_size_increment: 10_000)
165+
]
166+
end
167+
168+
test "is a no-op when the new size equals the current size", %{conn: conn} do
169+
assert {:ok, ^conn} = HTTP2.set_window_size(conn, :connection, 65_535)
170+
171+
# Nothing should have gone out on the wire.
172+
assert_recv_frames []
173+
end
174+
175+
test "returns an error when attempting to shrink the connection window", %{conn: conn} do
176+
{:ok, conn} = HTTP2.set_window_size(conn, :connection, 1_000_000)
177+
assert_recv_frames [window_update(stream_id: 0)]
178+
179+
assert {:error, ^conn, error} = HTTP2.set_window_size(conn, :connection, 500_000)
180+
181+
assert_http2_error error, {:window_size_too_small, 1_000_000, 500_000}
182+
183+
# No WINDOW_UPDATE was sent for the invalid call.
184+
assert_recv_frames []
185+
end
186+
187+
test "returns an error when attempting to shrink a stream window", %{conn: conn} do
188+
{conn, ref} = open_request(conn)
189+
assert_recv_frames [headers(stream_id: stream_id)]
190+
191+
current = conn.streams[stream_id].receive_window_size
192+
after_grow = current + 10_000
193+
shrink_target = current + 5_000
194+
195+
{:ok, conn} = HTTP2.set_window_size(conn, {:request, ref}, after_grow)
196+
assert_recv_frames [window_update(stream_id: ^stream_id)]
197+
198+
assert {:error, ^conn, error} =
199+
HTTP2.set_window_size(conn, {:request, ref}, shrink_target)
200+
201+
assert_http2_error error, {:window_size_too_small, ^after_grow, ^shrink_target}
202+
end
203+
204+
test "returns an error for an unknown request ref", %{conn: conn} do
205+
fake_ref = make_ref()
206+
207+
assert {:error, ^conn, error} = HTTP2.set_window_size(conn, {:request, fake_ref}, 1_000_000)
208+
209+
assert_http2_error error, {:unknown_request_to_stream, ^fake_ref}
210+
end
211+
212+
test "raises on out-of-range new_size", %{conn: conn} do
213+
assert_raise ArgumentError, ~r/1\.\.2147483647/, fn ->
214+
HTTP2.set_window_size(conn, :connection, 0)
215+
end
216+
217+
assert_raise ArgumentError, ~r/1\.\.2147483647/, fn ->
218+
HTTP2.set_window_size(conn, :connection, 3_000_000_000)
219+
end
220+
221+
assert_raise ArgumentError, ~r/1\.\.2147483647/, fn ->
222+
HTTP2.set_window_size(conn, :connection, :nope)
223+
end
224+
end
134225
end
135226

136227
describe "open?/1" do

0 commit comments

Comments
 (0)