lib/protocol/http2/dependency.rb
# frozen_string_literal: true # Released under the MIT License. # Copyright, 2020-2023, by Samuel Williams. module Protocol module HTTP2 DEFAULT_WEIGHT = 16 class Dependency def self.create(connection, id, priority = nil) weight = DEFAULT_WEIGHT exclusive = false if priority if parent = connection.dependencies[priority.stream_dependency] exclusive = priority.exclusive end weight = priority.weight end if parent.nil? parent = connection.dependency end dependency = self.new(connection, id, weight) connection.dependencies[id] = dependency if exclusive parent.exclusive_child(dependency) else parent.add_child(dependency) end return dependency end def initialize(connection, id, weight = DEFAULT_WEIGHT) @connection = connection @id = id @parent = nil @children = nil @weight = weight # Cache of any associated stream: @stream = nil # Cache of children for window allocation: @total_weight = 0 @ordered_children = nil end def <=> other @weight <=> other.weight end # The connection this stream belongs to. attr :connection # Stream ID (odd for client initiated streams, even otherwise). attr :id # The parent dependency. attr_accessor :parent # The dependent children. attr_accessor :children # The weight of the stream relative to other siblings. attr_accessor :weight def stream @stream ||= @connection.streams[@id] end def clear_cache! @ordered_children = nil end def delete! @connection.dependencies.delete(@id) @parent.remove_child(self) @children&.each do |id, child| parent.add_child(child) end @connection = nil @parent = nil @children = nil end def add_child(dependency) @children ||= {} @children[dependency.id] = dependency dependency.parent = self self.clear_cache! end def remove_child(dependency) @children&.delete(dependency.id) self.clear_cache! end # 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. # @param parent [Dependency] the dependency which will be inserted, taking control of all current children. def exclusive_child(parent) parent.children = @children @children&.each_value do |child| child.parent = parent end parent.clear_cache! @children = {parent.id => parent} self.clear_cache! parent.parent = self end def process_priority(priority) dependent_id = priority.stream_dependency if dependent_id == @id raise ProtocolError, "Stream priority for stream id #{@id} cannot depend on itself!" end @weight = priority.weight # We essentially ignore `dependent_id` if the dependency does not exist: if parent = @connection.dependencies[dependent_id] if priority.exclusive @parent.remove_child(self) parent.exclusive_child(self) elsif !@parent.equal?(parent) @parent.remove_child(self) parent.add_child(self) end end end # Change the priority of the stream both locally and remotely. def priority= priority send_priority(priority) process_priority(priority) end # The current local priority of the stream. def priority(exclusive = false) Priority.new(exclusive, @parent.id, @weight) end def send_priority(priority) @connection.send_priority(@id, priority) end def receive_priority(frame) self.process_priority(frame.unpack) end def total_weight self.ordered_children return @total_weight end def ordered_children unless @ordered_children if @children and !@children.empty? @ordered_children = @children.values.sort @total_weight = @ordered_children.sum(&:weight) end end return @ordered_children end # Traverse active streams in order of priority and allow them to consume the available flow-control window. # @param amount [Integer] the amount of data to write. Defaults to the current window capacity. def consume_window(size) # If there is an associated stream, give it priority: if stream = self.stream return if stream.window_updated(size) end # Otherwise, allow the dependent children to use up the available window: self.ordered_children&.each do |child| # Compute the proportional allocation: allocated = (child.weight * size) / @total_weight child.consume_window(allocated) if allocated > 0 end end def inspect "\#<#{self.class} id=#{@id} parent id=#{@parent&.id} weight=#{@weight} #{@children&.size || 0} children>" end def print_hierarchy(output = $stderr, indent: 0) output.puts "#{"\t" * indent}#{self.inspect}" @children&.each_value do |child| child.print_hierarchy(output, indent: indent+1) end end end end end