protocol-http2 0.20.0 → 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
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