diff --git a/lib/protocol/http2/connection.rb b/lib/protocol/http2/connection.rb index 0b65fa9..51a483c 100644 --- a/lib/protocol/http2/connection.rb +++ b/lib/protocol/http2/connection.rb @@ -41,7 +41,7 @@ def initialize(framer, local_stream_id) @decoder = HPACK::Context.new @encoder = HPACK::Context.new - @local_window = LocalWindow.new() + @local_window = LocalWindow.new @remote_window = Window.new end diff --git a/lib/protocol/http2/flow_controlled.rb b/lib/protocol/http2/flow_controlled.rb index b045af7..f9cebef 100644 --- a/lib/protocol/http2/flow_controlled.rb +++ b/lib/protocol/http2/flow_controlled.rb @@ -70,15 +70,11 @@ def send_window_update(window_increment) def receive_window_update(frame) amount = frame.unpack - # Async.logger.info(self) {"expanding remote_window=#{@remote_window} by #{amount}"} - if amount != 0 @remote_window.expand(amount) else raise ProtocolError, "Invalid window size increment: #{amount}!" end - - # puts "expanded remote_window=#{@remote_window} by #{amount}" end # The window has been expanded by the given amount. diff --git a/lib/protocol/http2/window.rb b/lib/protocol/http2/window.rb index a1d3a7c..8579aea 100644 --- a/lib/protocol/http2/window.rb +++ b/lib/protocol/http2/window.rb @@ -6,8 +6,11 @@ module Protocol module HTTP2 class Window + # When an HTTP/2 connection is first established, new streams are created with an initial flow-control window size of 65,535 octets. The connection flow-control window is also 65,535 octets. + DEFAULT_CAPACITY = 0xFFFF + # @param capacity [Integer] The initial window size, typically from the settings. - def initialize(capacity = 0xFFFF) + def initialize(capacity = DEFAULT_CAPACITY) # This is the main field required: @available = capacity @@ -75,12 +78,15 @@ def inspect # This is a window which efficiently maintains a desired capacity. class LocalWindow < Window - def initialize(capacity = 0xFFFF, desired: nil) + def initialize(capacity = DEFAULT_CAPACITY, desired: nil) super(capacity) + # The desired capacity of the window, may be bigger than the initial capacity. + # If that is the case, we will likely send a window update to the remote end to increase the capacity. @desired = desired end + # The desired capacity of the window. attr_accessor :desired def wanted @@ -88,17 +94,22 @@ def wanted # We must send an update which allows at least @desired bytes to be sent. (@desired - @capacity) + @used else - @used + super end end def limited? if @desired - @available < @desired + # Do not send window updates until we are less than half the desired capacity: + @available < (@desired / 2) else super end end + + def inspect + "\#<#{self.class} used=#{@used} available=#{@available} capacity=#{@capacity} desired=#{@desired}>" + end end end end diff --git a/test/protocol/http2/window.rb b/test/protocol/http2/window.rb index 3ffd7f7..b787ad9 100644 --- a/test/protocol/http2/window.rb +++ b/test/protocol/http2/window.rb @@ -4,6 +4,7 @@ # Copyright, 2019-2024, by Samuel Williams. require "protocol/http2/connection_context" +require "json" describe Protocol::HTTP2::Window do let(:window) {subject.new} @@ -54,5 +55,30 @@ window.consume(window.available) expect(window.wanted).to be == 200 end + + it "is not limited if the half the desired capacity is available" do + expect(window).not.to be(:limited?) + + # Consume the entire window: + window.consume(window.available) + + expect(window).to be(:limited?) + + # Expand the window by at least half the desired capacity: + window.expand(window.desired / 2) + + expect(window).not.to be(:limited?) + end + end + + with "#limited?" do + it "becomes limited after half the capacity is consumed" do + expect(window).not.to be(:limited?) + + # Consume a little more than half: + window.consume(window.capacity / 2 + 2) + + expect(window).to be(:limited?) + end end end diff --git a/test/protocol/http2/window_update_frame.rb b/test/protocol/http2/window_update_frame.rb index 3616632..f019e5c 100644 --- a/test/protocol/http2/window_update_frame.rb +++ b/test/protocol/http2/window_update_frame.rb @@ -203,8 +203,12 @@ def before expect(frame).to be_a Protocol::HTTP2::WindowUpdateFrame end - expect(client).to receive(:receive_window_update) + expect(client).to receive(:receive_window_update).twice + stream.send_data("*" * client.available_size) + expect(server.read_frame).to be_a Protocol::HTTP2::DataFrame + + frame = client.read_frame expect(frame).to be_a(Protocol::HTTP2::WindowUpdateFrame) expect(frame).to be(:connection?) # stream_id = 0 @@ -237,5 +241,20 @@ def before end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot update window of idle stream/) end end - end + + with "desired capacity" do + it "should send window updates only as needed" do + expect(client.local_window.desired).to be == 0xFFFF + + server_stream = server[stream.id] + + # Send a data frame that will consume less than half of the desired capacity: + server_stream.send_data("*" * 0xFF) + + expect(client.read_frame).to be_a Protocol::HTTP2::DataFrame + expect(client.local_window.used).to be == 0xFF + expect(client.local_window).not.to be(:limited?) + end + end + end end