refactor: move H2Connection instance to stream

- Remove all wrapper funtions made such that stream can send header/data
to H2Connection as they were not necessary

BREAKING CHANGES
Looks like, for small set of response data the StreamEnded event is
emitted and everything works well -- tested for both GET & POST request.
Maybe some issue with window size and/or flow control as when the
response data needs to be broken into separate chunks -- not all chunks
are received everytime which leads to indefinite waiting for next data
chunk and the connection is lost due to timeout. 😥

Working on setting up testing environment now. After testing is setup
I'll debug the above bug furthur.
This commit is contained in:
Aditya 2020-06-13 22:29:16 +05:30
parent d09ccf8d3b
commit d06bb12e35
2 changed files with 24 additions and 66 deletions

View File

@ -4,8 +4,7 @@ from typing import Dict, List
from h2.config import H2Configuration
from h2.connection import H2Connection
from h2.events import (
ConnectionTerminated, DataReceived, ResponseReceived, RemoteSettingsChanged,
StreamEnded, StreamReset, TrailersReceived, WindowUpdated
ConnectionTerminated, DataReceived, ResponseReceived, StreamEnded, StreamReset, WindowUpdated
)
from twisted.internet.protocol import connectionDone, Protocol
@ -13,7 +12,6 @@ from scrapy.core.http2.stream import Stream
from scrapy.http import Request
LOGGER = logging.getLogger(__name__)
LOGGER.debug = print
class H2ClientProtocol(Protocol):
@ -44,19 +42,27 @@ class H2ClientProtocol(Protocol):
def _new_stream(self, request: Request):
"""Instantiates a new Stream object
"""
stream = Stream(self.next_stream_id, request, self)
stream = Stream(self.next_stream_id, request, self.conn)
self.next_stream_id += 2
self.streams[stream.stream_id] = stream
return stream
def _send_pending_requests(self):
# TODO: handle MAX_CONCURRENT_STREAMS
# Initiate all pending requests
for stream in self._pending_request_stream_pool:
stream.initiate_request()
self._write_to_transport()
self._pending_request_stream_pool.clear()
def _write_to_transport(self):
""" Write data to the underlying transport connection
from the HTTP2 connection instance if any
"""
data = self.conn.data_to_send()
if data:
self.transport.write(data)
self.transport.write(data)
def request(self, _request: Request):
stream = self._new_stream(_request)
@ -75,10 +81,11 @@ class H2ClientProtocol(Protocol):
"""Called by Twisted when the connection is established. We can start
sending some data now: we should open with the connection preamble.
"""
LOGGER.debug("Connection made to {}".format(self.transport))
self.conn.initiate_connection()
self._write_to_transport()
self._send_pending_requests()
self.is_connection_made = True
def dataReceived(self, data):
@ -89,7 +96,6 @@ class H2ClientProtocol(Protocol):
def connectionLost(self, reason=connectionDone):
"""Called by Twisted when the transport connection is lost.
"""
LOGGER.debug(f"connectionLost {reason}")
stream_ids = list(self.streams.keys())
for stream in self._pending_request_stream_pool:
@ -119,47 +125,10 @@ class H2ClientProtocol(Protocol):
self.stream_ended(event)
elif isinstance(event, StreamReset):
self.stream_reset(event)
elif isinstance(event, TrailersReceived):
self.trailers_received(event)
elif isinstance(event, WindowUpdated):
self.window_updated(event)
elif isinstance(event, RemoteSettingsChanged):
self.remote_settings_changed(event)
def send_headers(self, stream_id, headers):
"""Send the headers for a given stream to the resource
Initiates a new connection hence.
This function is wrapper for :func:`~h2.connection.H2Connection.send_headers`
Arguments:
stream_id {int} -- Valid stream id
headers {List[Tuple[str, str]]} -- Headers of the request
"""
LOGGER.debug(f'Send Headers: stream_id={stream_id} headers={headers}')
self.conn.send_headers(stream_id, headers, end_stream=False)
def send_data(self, stream_id, data):
"""Send the data for a given stream to the resource.
Requires request headers to be sent at least once before this
function is called.
This function is wrapper for :func:`~h2.connection.H2Connection.send_data`
Arguments:
stream_id {int} -- Valid stream id
data {bytes} -- The data to send on the stream.
"""
LOGGER.debug(f"Send Data: stream_id={stream_id} data={data}")
self.conn.send_data(stream_id, data, end_stream=False)
def end_stream(self, stream_id):
"""End the given stream.
This function is wrapper for :func:`~h2.connection.H2Connection.end_stream`
Arguments:
stream_id {int} - Valid stream id
"""
LOGGER.debug(f"End Stream: stream_id={stream_id}")
self.conn.end_stream(stream_id)
else:
LOGGER.info("Received unhandled event {}".format(event))
# Event handler functions starts here
def connection_terminated(self, event: ConnectionTerminated):
@ -173,14 +142,6 @@ class H2ClientProtocol(Protocol):
stream_id = event.stream_id
self.streams[stream_id].receive_headers(event.headers)
def remote_settings_changed(self, event: RemoteSettingsChanged):
# TODO: handle MAX_CONCURRENT_STREAMS
# Initiate all pending requests
for stream in self._pending_request_stream_pool:
stream.initiate_request()
self._pending_request_stream_pool.clear()
def stream_ended(self, event: StreamEnded):
stream_id = event.stream_id
self.streams[stream_id].end_stream()
@ -188,9 +149,6 @@ class H2ClientProtocol(Protocol):
def stream_reset(self, event: StreamReset):
pass
def trailers_received(self, event: TrailersReceived):
pass
def window_updated(self, event: WindowUpdated):
stream_id = event.stream_id
if stream_id != 0:

View File

@ -1,5 +1,6 @@
from urllib.parse import urlparse
from h2.connection import H2Connection
from twisted.internet.defer import Deferred
from scrapy.http import Request, Response
@ -17,7 +18,7 @@ class Stream:
1. Combine all the data frames
"""
def __init__(self, stream_id: int, request: Request, connection):
def __init__(self, stream_id: int, request: Request, connection: H2Connection):
"""
Arguments:
stream_id {int} -- For one HTTP/2 connection each stream is
@ -27,7 +28,7 @@ class Stream:
"""
self.stream_id = stream_id
self._request = request
self._client_protocol = connection
self._conn = connection
self._request_body = self._request.body
self.content_length = 0 if self._request_body is None else len(self._request_body)
@ -70,13 +71,12 @@ class Stream:
# TODO: Check if scheme can be "http" for HTTP/2 ?
(":scheme", "https"),
(":path", url.path),
# ("Content-Length", str(self.content_length))
# TODO: Make sure 'Content-Type' and 'Content-Encoding' headers
# are sent for request having body
]
self._client_protocol.send_headers(self.stream_id, http2_request_headers)
self._conn.send_headers(self.stream_id, http2_request_headers, end_stream=False)
self.send_data()
def send_data(self):
@ -95,10 +95,10 @@ class Stream:
# 3.2 Small number of requests
# Firstly, check what the flow control window is for current stream.
window_size = self._client_protocol.conn.local_flow_control_window(stream_id=self.stream_id)
window_size = self._conn.local_flow_control_window(stream_id=self.stream_id)
# Next, check what the maximum frame size is.
max_frame_size = self._client_protocol.conn.max_outbound_frame_size
max_frame_size = self._conn.max_outbound_frame_size
# We will send no more than the window size or the remaining file size
# of data in this call, whichever is smaller.
@ -111,14 +111,14 @@ class Stream:
data_chunk_start = self.content_length - self.remaining_content_length
data_chunk = self._request_body[data_chunk_start:data_chunk_start + chunk_size]
self._client_protocol.send_data(self.stream_id, data_chunk, end_stream=False)
self._conn.send_data(self.stream_id, data_chunk, end_stream=False)
bytes_to_send = max(0, bytes_to_send - chunk_size)
self.remaining_content_length = max(0, self.remaining_content_length - chunk_size)
# End the stream if no more data has to be send
if self.remaining_content_length == 0:
self._client_protocol.end_stream(self.stream_id)
self._conn.end_stream(self.stream_id)
else:
# TODO: Continue from here :)
pass