module Connection

Provides shared connection behaviour for HTTP/2 client and server connections.

Definitions

def initialize(...)

Initialize the connection state.

Implementation

def initialize(...)
	super
	
	@reader = nil
	
	# Writing multiple frames at the same time can cause odd problems if frames are only partially written. So we use a semaphore to ensure frames are written in their entirety.
	@write_frame_guard = Async::Semaphore.new(1)
end

def synchronize(&block)

Synchronize write access to the connection.

Signature

yields {|...| ...}

The block to execute while holding the write lock.

Implementation

def synchronize(&block)
	@write_frame_guard.acquire(&block)
end

def write_frame(frame)

Write a single frame, deferring stop until the frame is written.

Implementation

def write_frame(frame)
	Task.current.defer_stop{super}
end

def write_frames

Write multiple frames, deferring stop until the frames are written.

Implementation

def write_frames
	Task.current.defer_stop{super}
end

def to_s

Signature

returns String

A string representation of this connection.

Implementation

def to_s
	"\#<#{self.class} #{@streams.count} active streams>"
end

def as_json(...)

Signature

returns String

A JSON-compatible representation.

Implementation

def as_json(...)
	to_s
end

def to_json(...)

Signature

returns String

A JSON string representation.

Implementation

def to_json(...)
	as_json.to_json(...)
end

def http1?

Signature

returns Boolean

Whether this is an HTTP/1 connection.

Implementation

def http1?
	false
end

def http2?

Signature

returns Boolean

Whether this is an HTTP/2 connection.

Implementation

def http2?
	true
end

def start_connection

Start the background reader task if it is not already running.

Implementation

def start_connection
	@reader || read_in_background
end

def close(error = nil)

Close the connection and stop the background reader.

Implementation

def close(error = nil)
	if reader = @reader
		@reader = nil
		
		# The reader task can close the connection itself, e.g. when the last stream completes and the connection is released back to the pool. Stopping it here would cancel the current task in the middle of this method, leaving the underlying stream open, so we let it unwind by itself: `closed?` is now true, so the read loop exits.
		reader.stop unless reader.current?
	end
	
	super
end

def close_if_drained!

The connection has finished draining the streams which the remote peer accepted before its graceful GOAWAY.

The last of those streams can complete on the sending side, in a task other than the background reader - the response arrived first and the request body was still being written. The reader is then parked in a blocking read and will never notice that the connection is closed, so we stop it and let its ensure close the connection.

Implementation

def close_if_drained!
	super
	
	if self.closed? and (reader = @reader) and !reader.current?
		reader.stop
	end
end

def read_in_background(parent: Task.current)

Start a transient background task that reads frames from the connection.

Implementation

def read_in_background(parent: Task.current)
	raise RuntimeError, "Connection is closed!" if closed?
	
	parent.async(transient: true) do |task|
		@reader = task
		
		task.annotate("#{version} reading data for #{self.class}.")
		
		# We don't need to defer stop here as this is already a transient task (ignores stop):
		begin
			while !self.closed?
				self.consume_window
				self.read_frame
			end
		rescue => error
			# Close with error.
		ensure
			# Don't call #close twice.
			if @reader
				@reader = nil
				
				self.close(error)
			end
		end
	end
end

def peer

Signature

returns Protocol::HTTP::Peer

The peer information for this connection.

Implementation

def peer
	@peer ||= ::Protocol::HTTP::Peer.for(@stream.io)
end

def concurrency

Signature

returns Integer

The maximum number of concurrent streams allowed.

Implementation

def concurrency
	self.maximum_concurrent_streams
end

def viable?

Can we use this connection to make requests?

Implementation

def viable?
	!self.goaway_received? && @stream&.readable?
end

def reusable?

Once the remote peer has sent a GOAWAY frame, it will not process any new streams on this connection, so it must not be handed out for another request, even while the streams it accepted are still being drained.

Signature

returns Boolean

Whether the connection can be reused.

Implementation

def reusable?
	!self.closed? && !self.goaway_received?
end

def version

Signature

returns String

The HTTP version string.

Implementation

def version
	VERSION
end