Skip to content

Commit 3aced03

Browse files
committed
Raise default HTTP/2 receive windows
Default connection receive window is now 16 MB (was 65_535), sent via a WINDOW_UPDATE on stream 0 as part of the connection preface. Default stream receive window is now 4 MB (was 65_535), advertised via SETTINGS_INITIAL_WINDOW_SIZE in the same preface. Both settable via the new `:connection_window_size` option and the existing `:client_settings` option. Window size / RTT sets a hard cap on per-stream throughput. At the previous 65_535-byte stream window: Path (typical RTT) | 65 KB | 4 MB | 16 MB -------------------------|----------|----------|---------- LAN (1 ms) | 62 MB/s | 4 GB/s | 16 GB/s Region (20 ms) | 3.1 MB/s | 200 MB/s | 800 MB/s Cross-country (70 ms) | 0.9 MB/s | 57 MB/s | 229 MB/s Transatlantic (100 ms) | 0.6 MB/s | 40 MB/s | 160 MB/s Transpacific (130 ms) | 0.5 MB/s | 31 MB/s | 123 MB/s Antipodal (230 ms) | 0.3 MB/s | 17 MB/s | 70 MB/s Any caller talking to a server more than a few milliseconds away was bottlenecked well below their link bandwidth without knowing why. 4 MB per stream saturates gigabit anywhere on earth; 16 MB at the connection level lets four streams run in parallel at full rate before the shared pool binds. Callers who want the old behaviour can pass `connection_window_size: 65_535` and `client_settings: [initial_window_size: 65_535]` to `connect/4`.
1 parent 26759f2 commit 3aced03

4 files changed

Lines changed: 105 additions & 11 deletions

File tree

lib/mint/http.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,11 @@ defmodule Mint.HTTP do
238238
server. See `Mint.HTTP2.put_settings/2` for more information. This is only used
239239
in HTTP/2 connections.
240240
241+
* `:connection_window_size` - (integer) the initial size of the connection-level
242+
HTTP/2 receive window, in bytes. Sent to the server as a `WINDOW_UPDATE` frame
243+
on stream 0 as part of the connection preface. Defaults to 16 MB. Can be
244+
raised later with `Mint.HTTP2.set_window_size/3`.
245+
241246
There may be further protocol specific options that only take effect when the corresponding
242247
connection is established. Check `Mint.HTTP1.connect/4` and `Mint.HTTP2.connect/4` for
243248
details.

lib/mint/http2.ex

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ defmodule Mint.HTTP2 do
143143
@transport_opts [alpn_advertised_protocols: ["h2"]]
144144

145145
@default_window_size 65_535
146+
@default_connection_window_size 16 * 1024 * 1024
147+
@default_stream_window_size 4 * 1024 * 1024
146148
@max_window_size 2_147_483_647
147149

148150
@default_max_frame_size 16_384
@@ -182,9 +184,10 @@ defmodule Mint.HTTP2 do
182184
send_window_size: @default_window_size,
183185
# `receive_window_size` is the client *receive* window for the
184186
# 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.
187+
# `WINDOW_UPDATE` frames on stream 0. Initialized to the configured
188+
# connection window during `initiate/5`, which also sends the
189+
# matching WINDOW_UPDATE to bring the server's view from the spec
190+
# default of 65_535 up to the advertised peak.
188191
receive_window_size: @default_window_size,
189192
encode_table: HPAX.new(4096),
190193
decode_table: HPAX.new(4096),
@@ -216,7 +219,7 @@ defmodule Mint.HTTP2 do
216219
# Settings that the client communicates to the server.
217220
client_settings: %{
218221
max_concurrent_streams: 100,
219-
initial_window_size: @default_window_size,
222+
initial_window_size: @default_stream_window_size,
220223
max_header_list_size: :infinity,
221224
max_frame_size: @default_max_frame_size,
222225
enable_push: true
@@ -1101,7 +1104,17 @@ defmodule Mint.HTTP2 do
11011104
scheme_string = Atom.to_string(scheme)
11021105
mode = Keyword.get(opts, :mode, :active)
11031106
log? = Keyword.get(opts, :log, false)
1107+
1108+
connection_window_size =
1109+
Keyword.get(opts, :connection_window_size, @default_connection_window_size)
1110+
1111+
validate_window_size!(:connection_window_size, connection_window_size)
1112+
11041113
client_settings_params = Keyword.get(opts, :client_settings, [])
1114+
1115+
client_settings_params =
1116+
Keyword.put_new(client_settings_params, :initial_window_size, @default_stream_window_size)
1117+
11051118
validate_client_settings!(client_settings_params)
11061119
# If the port is the default for the scheme, don't add it to the :authority pseudo-header
11071120
authority =
@@ -1130,12 +1143,13 @@ defmodule Mint.HTTP2 do
11301143
mode: mode,
11311144
scheme: scheme_string,
11321145
state: :handshaking,
1133-
log: log?
1146+
log: log?,
1147+
receive_window_size: connection_window_size
11341148
}
11351149

1150+
preface = build_preface(client_settings_params, connection_window_size)
1151+
11361152
with :ok <- Util.inet_opts(transport, socket),
1137-
client_settings = settings(stream_id: 0, params: client_settings_params),
1138-
preface = [@connection_preface, Frame.encode(client_settings)],
11391153
:ok <- transport.send(socket, preface),
11401154
conn = update_in(conn.client_settings_queue, &:queue.in(client_settings_params, &1)),
11411155
conn = put_in(conn.socket, socket),
@@ -1148,6 +1162,26 @@ defmodule Mint.HTTP2 do
11481162
end
11491163
end
11501164

1165+
defp build_preface(client_settings_params, connection_window_size) do
1166+
settings_frame = Frame.encode(settings(stream_id: 0, params: client_settings_params))
1167+
1168+
if connection_window_size > @default_window_size do
1169+
increment = connection_window_size - @default_window_size
1170+
update_frame = Frame.encode(window_update(stream_id: 0, window_size_increment: increment))
1171+
[@connection_preface, settings_frame, update_frame]
1172+
else
1173+
[@connection_preface, settings_frame]
1174+
end
1175+
end
1176+
1177+
defp validate_window_size!(name, value) do
1178+
unless is_integer(value) and value >= @default_window_size and value <= @max_window_size do
1179+
raise ArgumentError,
1180+
"the :#{name} option must be an integer in " <>
1181+
"#{@default_window_size}..#{@max_window_size}, got: #{inspect(value)}"
1182+
end
1183+
end
1184+
11511185
@doc """
11521186
See `Mint.HTTP.get_socket/1`.
11531187
"""

test/mint/http2/conn_test.exs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ defmodule Mint.HTTP2Test do
135135
end
136136

137137
describe "set_window_size/3" do
138+
@describetag connect_options: [connection_window_size: 65_535]
139+
138140
test "bumps the connection-level receive window by sending WINDOW_UPDATE on stream 0",
139141
%{conn: conn} do
140142
assert HTTP2.get_window_size(conn, :connection) == 65_535
@@ -1865,6 +1867,51 @@ defmodule Mint.HTTP2Test do
18651867
end
18661868
end
18671869

1870+
describe "default receive windows" do
1871+
test "advertises the configured connection window with a WINDOW_UPDATE in the preface",
1872+
%{conn: conn} do
1873+
# The default :connection_window_size is 16 MB; the preface carries
1874+
# a WINDOW_UPDATE for `16 MB - 65_535` so the server sees the peak
1875+
# from the start.
1876+
assert conn.receive_window_size == 16 * 1024 * 1024
1877+
end
1878+
1879+
test "advertises the configured stream window via SETTINGS", %{conn: conn} do
1880+
# The default :client_settings[:initial_window_size] is 4 MB.
1881+
assert conn.client_settings.initial_window_size == 4 * 1024 * 1024
1882+
1883+
{conn, _ref} = open_request(conn)
1884+
assert_recv_frames [headers(stream_id: stream_id)]
1885+
1886+
assert conn.streams[stream_id].receive_window_size == 4 * 1024 * 1024
1887+
end
1888+
1889+
@tag connect_options: [connection_window_size: 1_000_000]
1890+
test "supports a custom :connection_window_size", %{conn: conn} do
1891+
assert conn.receive_window_size == 1_000_000
1892+
end
1893+
1894+
@tag connect_options: [connection_window_size: 65_535]
1895+
test "omits the preface WINDOW_UPDATE when the configured window equals the spec default",
1896+
%{conn: conn} do
1897+
# At 65_535 there's nothing to advertise beyond SETTINGS — the
1898+
# preface should not carry an extra WINDOW_UPDATE.
1899+
assert conn.receive_window_size == 65_535
1900+
end
1901+
1902+
test "rejects a :connection_window_size below the spec minimum" do
1903+
assert_raise ArgumentError, ~r/:connection_window_size/, fn ->
1904+
HTTP2.initiate(:https, self(), "localhost", 443, connection_window_size: 1024)
1905+
end
1906+
end
1907+
1908+
test "rejects a :connection_window_size above 2^31-1" do
1909+
assert_raise ArgumentError, ~r/:connection_window_size/, fn ->
1910+
HTTP2.initiate(:https, self(), "localhost", 443, connection_window_size: 2_147_483_648)
1911+
end
1912+
end
1913+
end
1914+
18681915
describe "settings" do
18691916
test "put_settings/2 can be used to send settings to server", %{conn: conn} do
18701917
{:ok, conn} =

test/support/mint/http2/test_server.ex

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
defmodule Mint.HTTP2.TestServer do
22
import ExUnit.Assertions
3-
import Mint.HTTP2.Frame, only: [settings: 1, goaway: 1, ping: 1]
3+
import Mint.HTTP2.Frame, only: [settings: 1, goaway: 1, ping: 1, window_update: 1]
44

55
alias Mint.HTTP2.Frame
66

@@ -144,10 +144,18 @@ defmodule Mint.HTTP2.TestServer do
144144
# First we get the connection preface.
145145
{:ok, unquote(connection_preface) <> rest} = :ssl.recv(socket, 0, 100)
146146

147-
# Then we get a SETTINGS frame.
148-
assert {:ok, frame, ""} = Frame.decode_next(rest)
147+
# Then we get a SETTINGS frame, optionally followed by a WINDOW_UPDATE
148+
# on stream 0 if the client raised its connection-level receive window.
149+
assert {:ok, frame, rest} = Frame.decode_next(rest)
149150
assert settings(flags: ^no_flags, params: _params) = frame
150151

151-
:ok
152+
case rest do
153+
"" ->
154+
:ok
155+
156+
_ ->
157+
assert {:ok, window_update(stream_id: 0), ""} = Frame.decode_next(rest)
158+
:ok
159+
end
152160
end
153161
end

0 commit comments

Comments
 (0)