From c5d600237eeb00ba4e188d8980e2ac0b80ceb6a4 Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Wed, 8 Jun 2016 16:57:31 +0100 Subject: [PATCH 1/2] Add related_events property to some events. --- h2/events.py | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/h2/events.py b/h2/events.py index e749101b2..06b1e336c 100644 --- a/h2/events.py +++ b/h2/events.py @@ -23,6 +23,9 @@ class RequestReceived(object): .. versionchanged:: 2.3.0 Changed the type of ``headers`` to :class:`HeaderTuple `. This has no effect on current users. + + .. versionchanged:: 2.4.0 + Added ``related_events`` property. """ def __init__(self): #: The Stream ID for the stream this request was made on. @@ -31,6 +34,13 @@ def __init__(self): #: The request headers. self.headers = None + #: Any events "related" to this one: that is, any events that occurred + #: at the exact same time as this one because their data was carried on + #: the same frame. All of these events need to be processed at once. + #: + #: .. versionadded:: 2.4.0 + self.related_events = frozenset() + def __repr__(self): return "" % ( self.stream_id, self.headers @@ -46,6 +56,9 @@ class ResponseReceived(object): .. versionchanged:: 2.3.0 Changed the type of ``headers`` to :class:`HeaderTuple `. This has no effect on current users. + + .. versionchanged:: 2.4.0 + Added ``related_events`` property. """ def __init__(self): #: The Stream ID for the stream this response was made on. @@ -54,6 +67,13 @@ def __init__(self): #: The response headers. self.headers = None + #: Any events "related" to this one: that is, any events that occurred + #: at the exact same time as this one because their data was carried on + #: the same frame. All of these events need to be processed at once. + #: + #: .. versionadded:: 2.4.0 + self.related_events = frozenset() + def __repr__(self): return "" % ( self.stream_id, self.headers @@ -72,6 +92,9 @@ class TrailersReceived(object): .. versionchanged:: 2.3.0 Changed the type of ``headers`` to :class:`HeaderTuple `. This has no effect on current users. + + .. versionchanged:: 2.4.0 + Added ``related_events`` property. """ def __init__(self): #: The Stream ID for the stream on which these trailers were received. @@ -80,6 +103,13 @@ def __init__(self): #: The trailers themselves. self.headers = None + #: Any events "related" to this one: that is, any events that occurred + #: at the exact same time as this one because their data was carried on + #: the same frame. All of these events need to be processed at once. + #: + #: .. versionadded:: 2.4.0 + self.related_events = frozenset() + def __repr__(self): return "" % ( self.stream_id, self.headers @@ -104,6 +134,9 @@ class InformationalResponseReceived(object): .. versionchanged:: 2.3.0 Changed the type of ``headers`` to :class:`HeaderTuple `. This has no effect on current users. + + .. versionchanged:: 2.4.0 + Added ``related_events`` property. """ def __init__(self): #: The Stream ID for the stream this informational response was made @@ -113,6 +146,13 @@ def __init__(self): #: The headers for this informational response. self.headers = None + #: Any events "related" to this one: that is, any events that occurred + #: at the exact same time as this one because their data was carried on + #: the same frame. All of these events need to be processed at once. + #: + #: .. versionadded:: 2.4.0 + self.related_events = frozenset() + def __repr__(self): return "" % ( self.stream_id, self.headers @@ -124,6 +164,9 @@ class DataReceived(object): The DataReceived event is fired whenever data is received on a stream from the remote peer. The event carries the data itself, and the stream ID on which the data was received. + + .. versionchanged:: 2.4.0 + Added ``related_events`` property. """ def __init__(self): #: The Stream ID for the stream this data was received on. @@ -138,6 +181,13 @@ def __init__(self): #: than ``len(data)``. self.flow_controlled_length = None + #: Any events "related" to this one: that is, any events that occurred + #: at the exact same time as this one because their data was carried on + #: the same frame. All of these events need to be processed at once. + #: + #: .. versionadded:: 2.4.0 + self.related_events = frozenset() + def __repr__(self): return ( "" % self.stream_id @@ -333,6 +393,9 @@ class PriorityUpdated(object): This event is purely advisory, and does not need to be acted on. .. versionadded:: 2.0.0 + + .. versionchanged:: 2.4.0 + Added ``related_events`` property. """ def __init__(self): #: The ID of the stream whose priority information is being updated. @@ -350,6 +413,13 @@ def __init__(self): #: parent. self.exclusive = None + #: Any events "related" to this one: that is, any events that occurred + #: at the exact same time as this one because their data was carried on + #: the same frame. All of these events need to be processed at once. + #: + #: .. versionadded:: 2.4.0 + self.related_events = frozenset() + def __repr__(self): return ( " Date: Thu, 9 Jun 2016 16:54:02 +0100 Subject: [PATCH 2/2] Store related events. --- h2/connection.py | 11 +- test/test_related_events.py | 322 ++++++++++++++++++++++++++++++++++++ 2 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 test/test_related_events.py diff --git a/h2/connection.py b/h2/connection.py index 9974942b9..0d701bcf5 100644 --- a/h2/connection.py +++ b/h2/connection.py @@ -1296,7 +1296,16 @@ def receive_data(self, data): try: for frame in self.incoming_buffer: - events.extend(self._receive_frame(frame)) + frame_events = self._receive_frame(frame) + + # If more than one event popped out here, we want to process + # them as "related events". + if len(frame_events) > 1: + event_set = frozenset(frame_events) + for event in frame_events: + event.related_events = event_set + + events.extend(frame_events) except InvalidPaddingError: self._terminate_connection(PROTOCOL_ERROR) raise ProtocolError("Received frame with invalid padding.") diff --git a/test/test_related_events.py b/test/test_related_events.py new file mode 100644 index 000000000..00918faf9 --- /dev/null +++ b/test/test_related_events.py @@ -0,0 +1,322 @@ +# -*- coding: utf-8 -*- +""" +test_related_events.py +~~~~~~~~~~~~~~~~~~~~~~ + +Specific tests to validate the "related events" logic used by certain events +inside hyper-h2. +""" +import h2.connection +import h2.events + + +class TestRelatedEvents(object): + """ + Related events correlate all those events that happen on a single frame. + """ + example_request_headers = [ + (':authority', 'example.com'), + (':path', '/'), + (':scheme', 'https'), + (':method', 'GET'), + ] + + example_response_headers = [ + (':status', '200'), + ('server', 'fake-serv/0.1.0') + ] + + informational_response_headers = [ + (':status', '100'), + ('server', 'fake-serv/0.1.0') + ] + + example_trailers = [ + ('another', 'field'), + ] + + def test_request_received_related_all(self, frame_factory): + """ + RequestReceived has two possible related events: PriorityUpdated and + StreamEnded, all fired when a single HEADERS frame is received. + """ + c = h2.connection.H2Connection(client_side=False) + c.initiate_connection() + c.receive_data(frame_factory.preamble()) + + input_frame = frame_factory.build_headers_frame( + headers=self.example_request_headers, + flags=['END_STREAM', 'PRIORITY'], + stream_weight=15, + depends_on=0, + exclusive=False, + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 3 + set_events = frozenset(events) + + for event in events: + assert isinstance( + event, + ( + h2.events.RequestReceived, + h2.events.PriorityUpdated, + h2.events.StreamEnded, + ) + ) + assert event.related_events == set_events + + def test_request_received_related_priority(self, frame_factory): + """ + RequestReceived can be related to PriorityUpdated. + """ + c = h2.connection.H2Connection(client_side=False) + c.initiate_connection() + c.receive_data(frame_factory.preamble()) + + input_frame = frame_factory.build_headers_frame( + headers=self.example_request_headers, + flags=['PRIORITY'], + stream_weight=15, + depends_on=0, + exclusive=False, + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 2 + set_events = frozenset(events) + + for event in events: + assert isinstance( + event, (h2.events.RequestReceived, h2.events.PriorityUpdated) + ) + assert event.related_events == set_events + + def test_request_received_related_stream_ended(self, frame_factory): + """ + RequestReceived can be related to StreamEnded. + """ + c = h2.connection.H2Connection(client_side=False) + c.initiate_connection() + c.receive_data(frame_factory.preamble()) + + input_frame = frame_factory.build_headers_frame( + headers=self.example_request_headers, + flags=['END_STREAM'], + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 2 + set_events = frozenset(events) + + for event in events: + assert isinstance( + event, (h2.events.RequestReceived, h2.events.StreamEnded) + ) + assert event.related_events == set_events + + def test_response_received_related_all(self, frame_factory): + """ + ResponseReceived has two possible related events: PriorityUpdated and + StreamEnded, all fired when a single HEADERS frame is received. + """ + c = h2.connection.H2Connection() + c.initiate_connection() + c.send_headers(stream_id=1, headers=self.example_request_headers) + + input_frame = frame_factory.build_headers_frame( + headers=self.example_response_headers, + flags=['END_STREAM', 'PRIORITY'], + stream_weight=15, + depends_on=0, + exclusive=False, + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 3 + set_events = frozenset(events) + + for event in events: + assert isinstance( + event, + ( + h2.events.ResponseReceived, + h2.events.PriorityUpdated, + h2.events.StreamEnded, + ) + ) + assert event.related_events == set_events + + def test_response_received_related_priority(self, frame_factory): + """ + ResponseReceived can be related to PriorityUpdated. + """ + c = h2.connection.H2Connection() + c.initiate_connection() + c.send_headers(stream_id=1, headers=self.example_request_headers) + + input_frame = frame_factory.build_headers_frame( + headers=self.example_response_headers, + flags=['PRIORITY'], + stream_weight=15, + depends_on=0, + exclusive=False, + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 2 + set_events = frozenset(events) + + for event in events: + assert isinstance( + event, (h2.events.ResponseReceived, h2.events.PriorityUpdated) + ) + assert event.related_events == set_events + + def test_response_received_related_stream_ended(self, frame_factory): + """ + ResponseReceived can be related to StreamEnded. + """ + c = h2.connection.H2Connection() + c.initiate_connection() + c.send_headers(stream_id=1, headers=self.example_request_headers) + + input_frame = frame_factory.build_headers_frame( + headers=self.example_response_headers, + flags=['END_STREAM'], + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 2 + set_events = frozenset(events) + + for event in events: + assert isinstance( + event, (h2.events.ResponseReceived, h2.events.StreamEnded) + ) + assert event.related_events == set_events + + def test_trailers_received_related_all(self, frame_factory): + """ + TrailersReceived has two possible related events: PriorityUpdated and + StreamEnded, all fired when a single HEADERS frame is received. + """ + c = h2.connection.H2Connection() + c.initiate_connection() + c.send_headers(stream_id=1, headers=self.example_request_headers) + + f = frame_factory.build_headers_frame( + headers=self.example_response_headers, + ) + c.receive_data(f.serialize()) + + input_frame = frame_factory.build_headers_frame( + headers=self.example_trailers, + flags=['END_STREAM', 'PRIORITY'], + stream_weight=15, + depends_on=0, + exclusive=False, + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 3 + set_events = frozenset(events) + + for event in events: + assert isinstance( + event, + ( + h2.events.TrailersReceived, + h2.events.PriorityUpdated, + h2.events.StreamEnded, + ) + ) + assert event.related_events == set_events + + def test_trailers_received_related_stream_ended(self, frame_factory): + """ + TrailersReceived can be related to StreamEnded by itself. + """ + c = h2.connection.H2Connection() + c.initiate_connection() + c.send_headers(stream_id=1, headers=self.example_request_headers) + + f = frame_factory.build_headers_frame( + headers=self.example_response_headers, + ) + c.receive_data(f.serialize()) + + input_frame = frame_factory.build_headers_frame( + headers=self.example_trailers, + flags=['END_STREAM'], + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 2 + set_events = frozenset(events) + + for event in events: + assert isinstance( + event, (h2.events.TrailersReceived, h2.events.StreamEnded) + ) + assert event.related_events == set_events + + def test_informational_response_received_related_all(self, frame_factory): + """ + InformationalResponseReceived has one possible related event: + PriorityUpdated, fired when a single HEADERS frame is received. + """ + c = h2.connection.H2Connection() + c.initiate_connection() + c.send_headers(stream_id=1, headers=self.example_request_headers) + + input_frame = frame_factory.build_headers_frame( + headers=self.informational_response_headers, + flags=['PRIORITY'], + stream_weight=15, + depends_on=0, + exclusive=False, + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 2 + set_events = frozenset(events) + + for event in events: + assert isinstance( + event, + ( + h2.events.InformationalResponseReceived, + h2.events.PriorityUpdated, + ) + ) + assert event.related_events == set_events + + def test_data_received_related_stream_ended(self, frame_factory): + """ + DataReceived can be related to StreamEnded by itself. + """ + c = h2.connection.H2Connection() + c.initiate_connection() + c.send_headers(stream_id=1, headers=self.example_request_headers) + + f = frame_factory.build_headers_frame( + headers=self.example_response_headers, + ) + c.receive_data(f.serialize()) + + input_frame = frame_factory.build_data_frame( + data=b'some data', + flags=['END_STREAM'], + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 2 + set_events = frozenset(events) + + for event in events: + assert isinstance( + event, (h2.events.DataReceived, h2.events.StreamEnded) + ) + assert event.related_events == set_events