protocol-http2 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80a795de54c7f452a70b82f310c4f0aa3bda4bd15f034f548114a41bfc8d1649
4
- data.tar.gz: 496e203587367c71ce4b0298ba96492d5ce4892842fdcbf57796031556ca3f1a
3
+ metadata.gz: 3d3ec4ba460a65fcf1aa11e9c3ce4d0f46fde1cb0e7fa9cc33667b9142eb3dc4
4
+ data.tar.gz: 0afd95c0651daf938a1b6e472376d38274dc7df70fdd38e9f28cfe6c657bc81f
5
5
  SHA512:
6
- metadata.gz: e4b587ae75dc73dbd99ff3bc20cafe6c55cb329ab275bac187572730de5d5365f2f8c521d88b32bffb80363736cf0cdc62d579d2495872f13982b4f1cd1fdfa0
7
- data.tar.gz: f2964d07b204b702ded73b1a3df961dad07c95989e78311732d4264184b59a6671f7bb81511e1d196c1dd9ccf18f81db1fd8064607c3156550191dc7cec8a5d2
6
+ metadata.gz: 205b7683de9faee706dead27493c76b247c0d06c489778f670e4c49f0ecde263ad8b3b7ac1f86312681f83aee8bfaf4f82390f5d43c6a2d79dabdbe3f7d06b6a
7
+ data.tar.gz: c72cfe3041d0c4edc6e0510a8dc04f88dd547ebff1e96b9322f7ac35a64850fe516677a7e3df8b26258d7f089f10a2a4504211da8871678920ef65aa32b8a76b
checksums.yaml.gz.sig CHANGED
Binary file
@@ -33,7 +33,9 @@ module Protocol
33
33
  yield if block_given?
34
34
 
35
35
  read_frame do |frame|
36
- raise ProtocolError, "First frame must be #{SettingsFrame}, but got #{frame.class}" unless frame.is_a? SettingsFrame
36
+ unless frame.is_a? SettingsFrame
37
+ raise ProtocolError, "First frame must be #{SettingsFrame}, but got #{frame.class}"
38
+ end
37
39
  end
38
40
  else
39
41
  raise ProtocolError, "Cannot send connection preface in state #{@state}"
@@ -5,10 +5,10 @@
5
5
  # Copyright, 2023, by Marco Concetto Rudilosso.
6
6
 
7
7
  require_relative "framer"
8
- require_relative "dependency"
9
8
  require_relative "flow_controlled"
10
9
 
11
10
  require "protocol/hpack"
11
+ require "protocol/http/header/priority"
12
12
 
13
13
  module Protocol
14
14
  module HTTP2
@@ -23,10 +23,6 @@ module Protocol
23
23
  # Hash(Integer, Stream)
24
24
  @streams = {}
25
25
 
26
- # Hash(Integer, Dependency)
27
- @dependency = Dependency.new(self, 0)
28
- @dependencies = {0 => @dependency}
29
-
30
26
  @framer = framer
31
27
 
32
28
  # The next stream id to use:
@@ -95,7 +91,6 @@ module Protocol
95
91
 
96
92
  def delete(id)
97
93
  @streams.delete(id)
98
- @dependencies[id]&.delete!
99
94
  end
100
95
 
101
96
  # Close the underlying framer and all streams.
@@ -402,24 +397,21 @@ module Protocol
402
397
  end
403
398
  end
404
399
 
405
- def send_priority(stream_id, priority)
406
- frame = PriorityFrame.new(stream_id)
407
- frame.pack(priority)
408
-
409
- write_frame(frame)
400
+ def receive_push_promise(frame)
401
+ raise ProtocolError, "Unable to receive push promise!"
410
402
  end
411
403
 
412
- # Sets the priority for an incoming stream.
413
- def receive_priority(frame)
414
- if dependency = @dependencies[frame.stream_id]
415
- dependency.receive_priority(frame)
416
- elsif idle_stream_id?(frame.stream_id)
417
- Dependency.create(self, frame.stream_id, frame.unpack)
404
+ def receive_priority_update(frame)
405
+ if frame.stream_id != 0
406
+ raise ProtocolError, "Invalid stream id: #{frame.stream_id}"
407
+ end
408
+
409
+ stream_id, value = frame.unpack
410
+
411
+ # Apparently you can set the priority of idle streams, but I'm not sure why that makes sense, so for now let's ignore it.
412
+ if stream = @streams[stream_id]
413
+ stream.priority = Protocol::HTTP::Header::Priority.new(value)
418
414
  end
419
- end
420
-
421
- def receive_push_promise(frame)
422
- raise ProtocolError, "Unable to receive push promise!"
423
415
  end
424
416
 
425
417
  def client_stream_id?(id)
@@ -470,23 +462,17 @@ module Protocol
470
462
  end
471
463
  end
472
464
 
473
- # Traverse active streams in order of priority and allow them to consume the available flow-control window.
474
- # @param amount [Integer] the amount of data to write. Defaults to the current window capacity.
465
+ # Traverse active streams and allow them to consume the available flow-control window.
466
+ # @parameter amount [Integer] the amount of data to write. Defaults to the current window capacity.
475
467
  def consume_window(size = self.available_size)
476
468
  # Return if there is no window to consume:
477
469
  return unless size > 0
478
470
 
479
- # Console.info(self) do |buffer|
480
- # @dependencies.each do |id, dependency|
481
- # buffer.puts "- #{dependency}"
482
- # end
483
- #
484
- # buffer.puts
485
- #
486
- # @dependency.print_hierarchy(buffer)
487
- # end
488
-
489
- @dependency.consume_window(size)
471
+ @streams.each_value do |stream|
472
+ if stream.active?
473
+ stream.window_updated(size)
474
+ end
475
+ end
490
476
  end
491
477
 
492
478
  def receive_window_update(frame)
@@ -78,7 +78,7 @@ module Protocol
78
78
  end
79
79
 
80
80
  # The window has been expanded by the given amount.
81
- # @param size [Integer] the maximum amount of data to send.
81
+ # @parameter size [Integer] the maximum amount of data to send.
82
82
  # @return [Boolean] whether the window update was used or not.
83
83
  def window_updated(size)
84
84
  return false
@@ -34,7 +34,7 @@ module Protocol
34
34
  # The base class does not have any specific type index:
35
35
  TYPE = nil
36
36
 
37
- # @param length [Integer] the length of the payload, or nil if the header has not been read yet.
37
+ # @parameter length [Integer] the length of the payload, or nil if the header has not been read yet.
38
38
  def initialize(stream_id = 0, flags = 0, type = self.class::TYPE, length = nil, payload = nil)
39
39
  @stream_id = stream_id
40
40
  @flags = flags
@@ -134,7 +134,7 @@ module Protocol
134
134
 
135
135
  # Decodes common 9-byte header.
136
136
  #
137
- # @param buffer [String]
137
+ # @parameter buffer [String]
138
138
  def self.parse_header(buffer)
139
139
  length_hi, length_lo, type, flags, stream_id = buffer.unpack(HEADER_FORMAT)
140
140
  length = (length_hi << LENGTH_HISHIFT) | length_lo
@@ -7,7 +7,6 @@ require_relative "error"
7
7
 
8
8
  require_relative "data_frame"
9
9
  require_relative "headers_frame"
10
- require_relative "priority_frame"
11
10
  require_relative "reset_stream_frame"
12
11
  require_relative "settings_frame"
13
12
  require_relative "push_promise_frame"
@@ -15,6 +14,7 @@ require_relative "ping_frame"
15
14
  require_relative "goaway_frame"
16
15
  require_relative "window_update_frame"
17
16
  require_relative "continuation_frame"
17
+ require_relative "priority_update_frame"
18
18
 
19
19
  module Protocol
20
20
  module HTTP2
@@ -22,7 +22,7 @@ module Protocol
22
22
  FRAMES = [
23
23
  DataFrame,
24
24
  HeadersFrame,
25
- PriorityFrame,
25
+ nil, # PriorityFrame is deprecated and ignored, instead consider using PriorityUpdateFrame instead.
26
26
  ResetStreamFrame,
27
27
  SettingsFrame,
28
28
  PushPromiseFrame,
@@ -30,6 +30,13 @@ module Protocol
30
30
  GoawayFrame,
31
31
  WindowUpdateFrame,
32
32
  ContinuationFrame,
33
+ nil,
34
+ nil,
35
+ nil,
36
+ nil,
37
+ nil,
38
+ nil,
39
+ PriorityUpdateFrame,
33
40
  ].freeze
34
41
 
35
42
  # Default connection "fast-fail" preamble string as defined by the spec.
@@ -6,7 +6,6 @@
6
6
  require_relative "frame"
7
7
  require_relative "padded"
8
8
  require_relative "continuation_frame"
9
- require_relative "priority_frame"
10
9
 
11
10
  module Protocol
12
11
  module HTTP2
@@ -41,23 +40,16 @@ module Protocol
41
40
  data = super
42
41
 
43
42
  if priority?
44
- priority = Priority.unpack(data)
43
+ # We no longer support priority frames, so strip the data:
45
44
  data = data.byteslice(5, data.bytesize - 5)
46
45
  end
47
46
 
48
- return priority, data
47
+ return data
49
48
  end
50
49
 
51
- def pack(priority, data, *arguments, **options)
50
+ def pack(data, *arguments, **options)
52
51
  buffer = String.new.b
53
52
 
54
- if priority
55
- buffer << priority.pack
56
- set_flags(PRIORITY)
57
- else
58
- clear_flags(PRIORITY)
59
- end
60
-
61
53
  buffer << data
62
54
 
63
55
  super(buffer, *arguments, **options)
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2019-2024, by Samuel Williams.
5
+
6
+ require_relative "frame"
7
+ require_relative "padded"
8
+ require_relative "continuation_frame"
9
+
10
+ module Protocol
11
+ module HTTP2
12
+ # The PRIORITY_UPDATE frame is used by clients to signal the initial priority of a response, or to reprioritize a response or push stream. It carries the stream ID of the response and the priority in ASCII text, using the same representation as the Priority header field value.
13
+ #
14
+ # +-+-------------+-----------------------------------------------+
15
+ # |R| Prioritized Stream ID (31) |
16
+ # +-+-----------------------------+-------------------------------+
17
+ # | Priority Field Value (*) ...
18
+ # +---------------------------------------------------------------+
19
+ #
20
+ class PriorityUpdateFrame < Frame
21
+ TYPE = 0x10
22
+ FORMAT = "N".freeze
23
+
24
+ def unpack
25
+ data = super
26
+
27
+ prioritized_stream_id = data.unpack1(FORMAT)
28
+
29
+ return prioritized_stream_id, data.byteslice(4, data.bytesize - 4)
30
+ end
31
+
32
+ def pack(prioritized_stream_id, data, **options)
33
+ super([prioritized_stream_id].pack(FORMAT) + data, **options)
34
+ end
35
+
36
+ def apply(connection)
37
+ connection.receive_priority_update(self)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -15,6 +15,7 @@ module Protocol
15
15
  MAXIMUM_FRAME_SIZE = 0x5
16
16
  MAXIMUM_HEADER_LIST_SIZE = 0x6
17
17
  ENABLE_CONNECT_PROTOCOL = 0x8
18
+ NO_RFC7540_PRIORITIES = 0x9
18
19
 
19
20
  ASSIGN = [
20
21
  nil,
@@ -26,8 +27,22 @@ module Protocol
26
27
  :maximum_header_list_size=,
27
28
  nil,
28
29
  :enable_connect_protocol=,
30
+ :no_rfc7540_priorities=,
29
31
  ]
30
32
 
33
+ def initialize
34
+ # These limits are taken from the RFC:
35
+ # https://tools.ietf.org/html/rfc7540#section-6.5.2
36
+ @header_table_size = 4096
37
+ @enable_push = 1
38
+ @maximum_concurrent_streams = 0xFFFFFFFF
39
+ @initial_window_size = 0xFFFF # 2**16 - 1
40
+ @maximum_frame_size = 0x4000 # 2**14
41
+ @maximum_header_list_size = 0xFFFFFFFF
42
+ @enable_connect_protocol = 0
43
+ @no_rfc7540_priorities = 0
44
+ end
45
+
31
46
  # Allows the sender to inform the remote endpoint of the maximum size of the header compression table used to decode header blocks, in octets.
32
47
  attr_accessor :header_table_size
33
48
 
@@ -90,16 +105,18 @@ module Protocol
90
105
  @enable_connect_protocol == 1
91
106
  end
92
107
 
93
- def initialize
94
- # These limits are taken from the RFC:
95
- # https://tools.ietf.org/html/rfc7540#section-6.5.2
96
- @header_table_size = 4096
97
- @enable_push = 1
98
- @maximum_concurrent_streams = 0xFFFFFFFF
99
- @initial_window_size = 0xFFFF # 2**16 - 1
100
- @maximum_frame_size = 0x4000 # 2**14
101
- @maximum_header_list_size = 0xFFFFFFFF
102
- @enable_connect_protocol = 0
108
+ attr :no_rfc7540_priorities
109
+
110
+ def no_rfc7540_priorities= value
111
+ if value == 0 or value == 1
112
+ @no_rfc7540_priorities = value
113
+ else
114
+ raise ProtocolError, "Invalid value for no_rfc7540_priorities: #{value}"
115
+ end
116
+ end
117
+
118
+ def no_rfc7540_priorities?
119
+ @no_rfc7540_priorities == 1
103
120
  end
104
121
 
105
122
  def update(changes)
@@ -109,40 +126,6 @@ module Protocol
109
126
  end
110
127
  end
111
128
  end
112
-
113
- def difference(other)
114
- changes = []
115
-
116
- if @header_table_size != other.header_table_size
117
- changes << [HEADER_TABLE_SIZE, @header_table_size]
118
- end
119
-
120
- if @enable_push != other.enable_push
121
- changes << [ENABLE_PUSH, @enable_push]
122
- end
123
-
124
- if @maximum_concurrent_streams != other.maximum_concurrent_streams
125
- changes << [MAXIMUM_CONCURRENT_STREAMS, @maximum_concurrent_streams]
126
- end
127
-
128
- if @initial_window_size != other.initial_window_size
129
- changes << [INITIAL_WINDOW_SIZE, @initial_window_size]
130
- end
131
-
132
- if @maximum_frame_size != other.maximum_frame_size
133
- changes << [MAXIMUM_FRAME_SIZE, @maximum_frame_size]
134
- end
135
-
136
- if @maximum_header_list_size != other.maximum_header_list_size
137
- changes << [MAXIMUM_HEADER_LIST_SIZE, @maximum_header_list_size]
138
- end
139
-
140
- if @enable_connect_protocol != other.enable_connect_protocol
141
- changes << [ENABLE_CONNECT_PROTOCOL, @enable_connect_protocol]
142
- end
143
-
144
- return changes
145
- end
146
129
  end
147
130
 
148
131
  class PendingSettings
@@ -4,7 +4,6 @@
4
4
  # Copyright, 2019-2024, by Samuel Williams.
5
5
 
6
6
  require_relative "connection"
7
- require_relative "dependency"
8
7
 
9
8
  module Protocol
10
9
  module HTTP2
@@ -78,7 +77,7 @@ module Protocol
78
77
  @local_window = Window.new(@connection.local_settings.initial_window_size)
79
78
  @remote_window = Window.new(@connection.remote_settings.initial_window_size)
80
79
 
81
- @dependency = Dependency.create(@connection, @id)
80
+ @priority = nil
82
81
  end
83
82
 
84
83
  # The connection this stream belongs to.
@@ -90,26 +89,11 @@ module Protocol
90
89
  # Stream state, e.g. `idle`, `closed`.
91
90
  attr_accessor :state
92
91
 
93
- attr :dependency
94
-
95
92
  attr :local_window
96
93
  attr :remote_window
97
94
 
98
- def weight
99
- @dependency.weight
100
- end
101
-
102
- def priority
103
- @dependency.priority
104
- end
105
-
106
- def priority= priority
107
- @dependency.priority = priority
108
- end
109
-
110
- def parent=(stream)
111
- @dependency.parent = stream.dependency
112
- end
95
+ # @attribute [Protocol::HTTP::Header::Priority | Nil] the priority of the stream.
96
+ attr_accessor :priority
113
97
 
114
98
  def maximum_frame_size
115
99
  @connection.available_frame_size
@@ -141,13 +125,13 @@ module Protocol
141
125
  @state == :idle or @state == :reserved_local or @state == :open or @state == :half_closed_remote
142
126
  end
143
127
 
144
- private def write_headers(priority, headers, flags = 0)
128
+ private def write_headers(headers, flags = 0)
145
129
  frame = HeadersFrame.new(@id, flags)
146
130
 
147
131
  @connection.write_frames do |framer|
148
132
  data = @connection.encode_headers(headers)
149
133
 
150
- frame.pack(priority, data, maximum_size: @connection.maximum_frame_size)
134
+ frame.pack(data, maximum_size: @connection.maximum_frame_size)
151
135
 
152
136
  framer.write_frame(frame)
153
137
  end
@@ -236,7 +220,7 @@ module Protocol
236
220
  end
237
221
 
238
222
  # Transition the stream into the closed state.
239
- # @param error_code [Integer] the error code if the stream was closed due to a stream reset.
223
+ # @parameter error_code [Integer] the error code if the stream was closed due to a stream reset.
240
224
  def close!(error_code = nil)
241
225
  @state = :closed
242
226
  @connection.delete(@id)
@@ -265,11 +249,7 @@ module Protocol
265
249
 
266
250
  def process_headers(frame)
267
251
  # Receiving request headers:
268
- priority, data = frame.unpack
269
-
270
- if priority
271
- @dependency.process_priority(priority)
272
- end
252
+ data = frame.unpack
273
253
 
274
254
  @connection.decode_headers(data)
275
255
  end
@@ -401,7 +381,7 @@ module Protocol
401
381
  end
402
382
 
403
383
  # Server push is semantically equivalent to a server responding to a request; however, in this case, that request is also sent by the server, as a PUSH_PROMISE frame.
404
- # @param headers [Hash] contains a complete set of request header fields that the server attributes to the request.
384
+ # @parameter headers [Hash] contains a complete set of request header fields that the server attributes to the request.
405
385
  def send_push_promise(headers)
406
386
  if @state == :open or @state == :half_closed_remote
407
387
  promised_stream = self.create_push_promise_stream(headers)
@@ -427,7 +407,6 @@ module Protocol
427
407
  headers = @connection.decode_headers(data)
428
408
 
429
409
  stream = self.accept_push_promise_stream(promised_stream_id, headers)
430
- stream.parent = self
431
410
  stream.reserved_remote!
432
411
 
433
412
  return stream, headers
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Protocol
7
7
  module HTTP2
8
- VERSION = "0.20.0"
8
+ VERSION = "0.22.0"
9
9
  end
10
10
  end
@@ -9,7 +9,7 @@ module Protocol
9
9
  # 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.
10
10
  DEFAULT_CAPACITY = 0xFFFF
11
11
 
12
- # @param capacity [Integer] The initial window size, typically from the settings.
12
+ # @parameter capacity [Integer] The initial window size, typically from the settings.
13
13
  def initialize(capacity = DEFAULT_CAPACITY)
14
14
  # This is the main field required:
15
15
  @available = capacity
data/readme.md CHANGED
@@ -58,7 +58,7 @@ Async do
58
58
  ]
59
59
 
60
60
  puts "Sending request on stream id=#{stream.id} state=#{stream.state}..."
61
- stream.send_headers(nil, headers, Protocol::HTTP2::END_STREAM)
61
+ stream.send_headers(headers, Protocol::HTTP2::END_STREAM)
62
62
 
63
63
  puts "Waiting for response..."
64
64
  $count = 0
data/releases.md ADDED
@@ -0,0 +1,9 @@
1
+ # Releases
2
+
3
+ ## Unreleased
4
+
5
+ ### Added Priority Update Frame and Stream Priority
6
+
7
+ HTTP/2 has deprecated the priority frame and stream dependency tracking. This feature has been effectively removed from the protocol. As a consequence, the internal implementation is greatly simplified. The `Protocol::HTTP2::Stream` class no longer tracks dependencies, and this includes `Stream#send_headers` which no longer takes `priority` as the first argument.
8
+
9
+ Optional per-request priority can be set using the `priority` header instead, and this value can be manipulated using the priority update frame.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protocol-http2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 0.22.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -40,7 +40,7 @@ cert_chain:
40
40
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
41
41
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
42
42
  -----END CERTIFICATE-----
43
- date: 2024-11-09 00:00:00.000000000 Z
43
+ date: 2024-12-01 00:00:00.000000000 Z
44
44
  dependencies:
45
45
  - !ruby/object:Gem::Dependency
46
46
  name: protocol-hpack
@@ -81,7 +81,6 @@ files:
81
81
  - lib/protocol/http2/connection.rb
82
82
  - lib/protocol/http2/continuation_frame.rb
83
83
  - lib/protocol/http2/data_frame.rb
84
- - lib/protocol/http2/dependency.rb
85
84
  - lib/protocol/http2/error.rb
86
85
  - lib/protocol/http2/flow_controlled.rb
87
86
  - lib/protocol/http2/frame.rb
@@ -90,7 +89,7 @@ files:
90
89
  - lib/protocol/http2/headers_frame.rb
91
90
  - lib/protocol/http2/padded.rb
92
91
  - lib/protocol/http2/ping_frame.rb
93
- - lib/protocol/http2/priority_frame.rb
92
+ - lib/protocol/http2/priority_update_frame.rb
94
93
  - lib/protocol/http2/push_promise_frame.rb
95
94
  - lib/protocol/http2/reset_stream_frame.rb
96
95
  - lib/protocol/http2/server.rb
@@ -103,6 +102,7 @@ files:
103
102
  - lib/traces/provider/protocol/http2/framer.rb
104
103
  - license.md
105
104
  - readme.md
105
+ - releases.md
106
106
  homepage: https://github.com/socketry/protocol-http2
107
107
  licenses:
108
108
  - MIT
metadata.gz.sig CHANGED
Binary file
@@ -1,245 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2020-2023, by Samuel Williams.
5
-
6
- module Protocol
7
- module HTTP2
8
- DEFAULT_WEIGHT = 16
9
-
10
- class Dependency
11
- def self.create(connection, id, priority = nil)
12
- weight = DEFAULT_WEIGHT
13
- exclusive = false
14
-
15
- if priority
16
- if parent = connection.dependencies[priority.stream_dependency]
17
- exclusive = priority.exclusive
18
- end
19
-
20
- weight = priority.weight
21
- end
22
-
23
- if parent.nil?
24
- parent = connection.dependency
25
- end
26
-
27
- dependency = self.new(connection, id, weight)
28
-
29
- connection.dependencies[id] = dependency
30
-
31
- if exclusive
32
- parent.exclusive_child(dependency)
33
- else
34
- parent.add_child(dependency)
35
- end
36
-
37
- return dependency
38
- end
39
-
40
- def initialize(connection, id, weight = DEFAULT_WEIGHT)
41
- @connection = connection
42
- @id = id
43
-
44
- @parent = nil
45
- @children = nil
46
-
47
- @weight = weight
48
-
49
- # Cache of any associated stream:
50
- @stream = nil
51
-
52
- # Cache of children for window allocation:
53
- @total_weight = 0
54
- @ordered_children = nil
55
- end
56
-
57
- def <=> other
58
- @weight <=> other.weight
59
- end
60
-
61
- def == other
62
- @id == other.id
63
- end
64
-
65
- # The connection this stream belongs to.
66
- attr :connection
67
-
68
- # Stream ID (odd for client initiated streams, even otherwise).
69
- attr :id
70
-
71
- # The parent dependency.
72
- attr_accessor :parent
73
-
74
- # The dependent children.
75
- attr_accessor :children
76
-
77
- # The weight of the stream relative to other siblings.
78
- attr_accessor :weight
79
-
80
- def stream
81
- @stream ||= @connection.streams[@id]
82
- end
83
-
84
- def clear_cache!
85
- @ordered_children = nil
86
- end
87
-
88
- def delete!
89
- @connection.dependencies.delete(@id)
90
-
91
- @parent.remove_child(self)
92
-
93
- @children&.each do |id, child|
94
- parent.add_child(child)
95
- end
96
-
97
- @connection = nil
98
- @parent = nil
99
- @children = nil
100
- end
101
-
102
- def add_child(dependency)
103
- @children ||= {}
104
- @children[dependency.id] = dependency
105
-
106
- dependency.parent = self
107
-
108
- if @ordered_children
109
- # Binary search for insertion point:
110
- index = @ordered_children.bsearch_index do |child|
111
- child.weight >= dependency.weight
112
- end
113
-
114
- if index
115
- @ordered_children.insert(index, dependency)
116
- else
117
- @ordered_children.push(dependency)
118
- end
119
-
120
- @total_weight += dependency.weight
121
- end
122
- end
123
-
124
- def remove_child(dependency)
125
- @children&.delete(dependency.id)
126
-
127
- if @ordered_children
128
- # Don't do a linear search here, it can be slow. Instead, the child's parent will be set to `nil`, and we check this in {#consume_window} using `delete_if`.
129
- # @ordered_children.delete(dependency)
130
- @total_weight -= dependency.weight
131
- end
132
- end
133
-
134
- # An exclusive flag allows for the insertion of a new level of dependencies. The exclusive flag causes the stream to become the sole dependency of its parent stream, causing other dependencies to become dependent on the exclusive stream.
135
- # @param parent [Dependency] the dependency which will be inserted, taking control of all current children.
136
- def exclusive_child(parent)
137
- parent.children = @children
138
-
139
- @children&.each_value do |child|
140
- child.parent = parent
141
- end
142
-
143
- parent.clear_cache!
144
-
145
- @children = {parent.id => parent}
146
- self.clear_cache!
147
-
148
- parent.parent = self
149
- end
150
-
151
- def process_priority(priority)
152
- dependent_id = priority.stream_dependency
153
-
154
- if dependent_id == @id
155
- raise ProtocolError, "Stream priority for stream id #{@id} cannot depend on itself!"
156
- end
157
-
158
- @weight = priority.weight
159
-
160
- # We essentially ignore `dependent_id` if the dependency does not exist:
161
- if parent = @connection.dependencies[dependent_id]
162
- if priority.exclusive
163
- @parent.remove_child(self)
164
-
165
- parent.exclusive_child(self)
166
- elsif !@parent.equal?(parent)
167
- @parent.remove_child(self)
168
-
169
- parent.add_child(self)
170
- end
171
- end
172
- end
173
-
174
- # Change the priority of the stream both locally and remotely.
175
- def priority= priority
176
- send_priority(priority)
177
- process_priority(priority)
178
- end
179
-
180
- # The current local priority of the stream.
181
- def priority(exclusive = false)
182
- Priority.new(exclusive, @parent.id, @weight)
183
- end
184
-
185
- def send_priority(priority)
186
- @connection.send_priority(@id, priority)
187
- end
188
-
189
- def receive_priority(frame)
190
- self.process_priority(frame.unpack)
191
- end
192
-
193
- def total_weight
194
- self.ordered_children
195
-
196
- return @total_weight
197
- end
198
-
199
- def ordered_children
200
- unless @ordered_children
201
- if @children and !@children.empty?
202
- @ordered_children = @children.values.sort
203
- @total_weight = @ordered_children.sum(&:weight)
204
- end
205
- end
206
-
207
- return @ordered_children
208
- end
209
-
210
- # Traverse active streams in order of priority and allow them to consume the available flow-control window.
211
- # @param amount [Integer] the amount of data to write. Defaults to the current window capacity.
212
- def consume_window(size)
213
- # If there is an associated stream, give it priority:
214
- if stream = self.stream
215
- return if stream.window_updated(size)
216
- end
217
-
218
- # Otherwise, allow the dependent children to use up the available window:
219
- self.ordered_children&.delete_if do |child|
220
- if child.parent
221
- # Compute the proportional allocation:
222
- allocated = (child.weight * size) / @total_weight
223
-
224
- child.consume_window(allocated) if allocated > 0
225
-
226
- false
227
- else
228
- true
229
- end
230
- end
231
- end
232
-
233
- def inspect
234
- "\#<#{self.class} id=#{@id} parent id=#{@parent&.id} weight=#{@weight} #{@children&.size || 0} children>"
235
- end
236
-
237
- def print_hierarchy(output = $stderr, indent: 0)
238
- output.puts "#{"\t" * indent}#{self.inspect}"
239
- @children&.each_value do |child|
240
- child.print_hierarchy(output, indent: indent+1)
241
- end
242
- end
243
- end
244
- end
245
- end
@@ -1,82 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
5
-
6
- require_relative "frame"
7
-
8
- module Protocol
9
- module HTTP2
10
- VALID_WEIGHT = (1..256)
11
-
12
- # Stream Dependency: A 31-bit stream identifier for the stream that
13
- # this stream depends on (see Section 5.3). This field is only
14
- # present if the PRIORITY flag is set.
15
- class Priority < Struct.new(:exclusive, :stream_dependency, :weight)
16
- FORMAT = "NC".freeze
17
- EXCLUSIVE = 1 << 31
18
-
19
- # All streams are initially assigned a non-exclusive dependency on stream 0x0. Pushed streams (Section 8.2) initially depend on their associated stream. In both cases, streams are assigned a default weight of 16.
20
- def self.default(stream_dependency = 0, weight = 16)
21
- self.new(false, stream_dependency, weight)
22
- end
23
-
24
- def self.unpack(data)
25
- stream_dependency, weight = data.unpack(FORMAT)
26
-
27
- # Weight: An unsigned 8-bit integer representing a priority weight for the stream (see Section 5.3). Add one to the value to obtain a weight between 1 and 256. This field is only present if the PRIORITY flag is set.
28
- return self.new(stream_dependency & EXCLUSIVE != 0, stream_dependency & ~EXCLUSIVE, weight + 1)
29
- end
30
-
31
- def pack
32
- if exclusive
33
- stream_dependency = self.stream_dependency | EXCLUSIVE
34
- else
35
- stream_dependency = self.stream_dependency
36
- end
37
-
38
- return [stream_dependency, self.weight - 1].pack(FORMAT)
39
- end
40
-
41
- def weight= value
42
- if VALID_WEIGHT.include?(value)
43
- super
44
- else
45
- raise ArgumentError, "Weight #{value} must be between 1-256!"
46
- end
47
- end
48
- end
49
-
50
- # The PRIORITY frame specifies the sender-advised priority of a stream. It can be sent in any stream state, including idle or closed streams.
51
- #
52
- # +-+-------------------------------------------------------------+
53
- # |E| Stream Dependency (31) |
54
- # +-+-------------+-----------------------------------------------+
55
- # | Weight (8) |
56
- # +-+-------------+
57
- #
58
- class PriorityFrame < Frame
59
- TYPE = 0x2
60
-
61
- def pack priority
62
- super priority.pack
63
- end
64
-
65
- def unpack
66
- Priority.unpack(super)
67
- end
68
-
69
- def apply(connection)
70
- connection.receive_priority(self)
71
- end
72
-
73
- def read_payload(stream)
74
- super
75
-
76
- if @length != 5
77
- raise FrameSizeError, "Invalid frame length"
78
- end
79
- end
80
- end
81
- end
82
- end