Skip to content

Commit c3a40a7

Browse files
authored
HTTP/2: Wait END_STREAM flag on half-closed(local) stream state (#12257)
* HTTP/2: Wait END_STREAM flag on half-closed(local) stream state * Actively close stream in half-closed(local) state * Add AuTest * Fix format of AuTests * Address comments
1 parent 132e01c commit c3a40a7

4 files changed

Lines changed: 232 additions & 11 deletions

File tree

src/proxy/http2/Http2ConnectionState.cc

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,15 @@ Http2ConnectionState::rcv_data_frame(const Http2Frame &frame)
175175

176176
// Pure END_STREAM
177177
if (payload_length == 0) {
178+
if (stream->get_state() == Http2StreamState::HTTP2_STREAM_STATE_CLOSED) {
179+
stream->initiating_close();
180+
return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_NONE);
181+
}
182+
178183
if (stream->is_read_enabled()) {
179184
stream->signal_read_event(VC_EVENT_READ_COMPLETE);
180185
}
186+
181187
return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_NONE);
182188
}
183189
} else {
@@ -2380,13 +2386,16 @@ Http2ConnectionState::send_data_frames(Http2Stream *stream)
23802386

23812387
if (result == Http2SendDataFrameResult::DONE) {
23822388
if (!stream->is_outbound_connection()) {
2383-
// Delete a stream immediately
2384-
// TODO its should not be deleted for a several time to handling
2385-
// RST_STREAM and WINDOW_UPDATE.
2386-
// See 'closed' state written at [RFC 7540] 5.1.
2387-
Http2StreamDebug(this->session, stream->get_id(), "Shutdown stream");
2388-
stream->signal_write_event(VC_EVENT_WRITE_COMPLETE);
2389-
stream->do_io_close();
2389+
if (stream->get_state() == Http2StreamState::HTTP2_STREAM_STATE_CLOSED) {
2390+
// Delete a stream immediately
2391+
Http2StreamDebug(this->session, stream->get_id(), "Shutdown stream");
2392+
stream->signal_write_event(VC_EVENT_WRITE_COMPLETE);
2393+
stream->do_io_close();
2394+
} else {
2395+
// This stream waits for the END_STREAM in half-closed (local) state until `http.transaction_no_activity_timeout_in`.
2396+
// If no frame with END_STREAM is found, ATS actively closes the stream.
2397+
Http2StreamDebug(this->session, stream->get_id(), "waiting END_STREAM");
2398+
}
23902399
} else if (stream->is_outbound_connection() && stream->is_write_vio_done()) {
23912400
stream->signal_write_event(VC_EVENT_WRITE_COMPLETE);
23922401
} else {

src/proxy/http2/Http2Stream.cc

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "tscore/Diags.h"
3232
#include "tscore/HTTPVersion.h"
3333
#include "tscore/ink_assert.h"
34+
#include "tsutil/DbgCtl.h"
3435

3536
#include <numeric>
3637

@@ -562,7 +563,10 @@ Http2Stream::do_io_close(int /* flags */)
562563
// We only need to do this for the client side since we only need to pass through RST_STREAM
563564
// from the server. If a client sends a RST_STREAM, we need to keep the server side alive so
564565
// the background fill can function as intended.
565-
if (!this->is_outbound_connection() && this->is_state_writeable()) {
566+
//
567+
// In the half-closed(local) state, server can close stream actively by sending RST_STREAM frame
568+
if (!this->is_outbound_connection() &&
569+
(this->is_state_writeable() || _state == Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_LOCAL)) {
566570
this->get_connection_state().send_rst_stream_frame(_id, Http2ErrorCode::HTTP2_ERROR_NO_ERROR);
567571
}
568572

@@ -590,7 +594,7 @@ Http2Stream::transaction_done()
590594
SCOPED_MUTEX_LOCK(lock, this->mutex, this_ethread());
591595
super::transaction_done();
592596

593-
if (!closed) {
597+
if (!closed && _state == Http2StreamState::HTTP2_STREAM_STATE_CLOSED) {
594598
do_io_close(); // Make sure we've been closed. If we didn't close the _proxy_ssn session better still be open
595599
}
596600
Http2ConnectionState &state = this->get_connection_state();
@@ -1081,11 +1085,20 @@ Http2Stream::clear_io_events()
10811085
}
10821086
}
10831087

1084-
// release and do_io_close are the same for the HTTP/2 protocol
1088+
/**
1089+
Callback from HttpSM
1090+
1091+
release and do_io_close are the same for the HTTP/2 protocol
1092+
*/
10851093
void
10861094
Http2Stream::release()
10871095
{
1088-
this->do_io_close();
1096+
if (_state == Http2StreamState::HTTP2_STREAM_STATE_CLOSED) {
1097+
this->do_io_close();
1098+
return;
1099+
}
1100+
1101+
Http2StreamDebug("Delaying do_io_close() until stream is in the closed state");
10891102
}
10901103

10911104
void
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/usr/bin/env python3
2+
'''
3+
HTTP/2 client that sends empty DATA frame with END_STREAM flag
4+
'''
5+
# Licensed to the Apache Software Foundation (ASF) under one
6+
# or more contributor license agreements. See the NOTICE file
7+
# distributed with this work for additional information
8+
# regarding copyright ownership. The ASF licenses this file
9+
# to you under the Apache License, Version 2.0 (the
10+
# "License"); you may not use this file except in compliance
11+
# with the License. You may obtain a copy of the License at
12+
#
13+
# http://www.apache.org/licenses/LICENSE-2.0
14+
#
15+
# Unless required by applicable law or agreed to in writing, software
16+
# distributed under the License is distributed on an "AS IS" BASIS,
17+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
# See the License for the specific language governing permissions and
19+
# limitations under the License.
20+
21+
import socket
22+
import ssl
23+
24+
import h2.connection
25+
import h2.events
26+
27+
import argparse
28+
29+
30+
# TODO: cleanup with other HTTP/2 clients (h2active_timeout.py and h2client.py)
31+
def get_socket(port: int) -> socket.socket:
32+
"""Create a TLS-wrapped socket.
33+
34+
:param port: The port to connect to.
35+
36+
:returns: A TLS-wrapped socket.
37+
"""
38+
39+
SERVER_NAME = 'localhost'
40+
SERVER_PORT = port
41+
42+
# generic socket and ssl configuration
43+
socket.setdefaulttimeout(15)
44+
45+
# Configure an ssl client side context which will not check the server's certificate.
46+
ctx = ssl.create_default_context()
47+
ctx.check_hostname = False
48+
ctx.verify_mode = ssl.CERT_NONE
49+
ctx.set_alpn_protocols(['h2'])
50+
51+
# open a socket to the server and initiate TLS/SSL
52+
tls_socket = socket.create_connection((SERVER_NAME, SERVER_PORT))
53+
tls_socket = ctx.wrap_socket(tls_socket, server_hostname=SERVER_NAME)
54+
return tls_socket
55+
56+
57+
def make_request(port: int, path: str, n: int) -> None:
58+
"""Establish an HTTP/2 connection and send a request.
59+
60+
:param port: The port to connect to.
61+
:param path: The path to request.
62+
:param n: Number of streams to open.
63+
"""
64+
65+
tls_socket = get_socket(port)
66+
67+
h2_connection = h2.connection.H2Connection()
68+
h2_connection.initiate_connection()
69+
tls_socket.sendall(h2_connection.data_to_send())
70+
71+
headers = [
72+
(':method', 'GET'),
73+
(':path', path),
74+
(':authority', 'localhost'),
75+
(':scheme', 'https'),
76+
]
77+
78+
for stream_id in range(1, n * 2, 2):
79+
h2_connection.send_headers(stream_id, headers, end_stream=False)
80+
h2_connection.send_data(stream_id, b'', end_stream=True)
81+
82+
tls_socket.sendall(h2_connection.data_to_send())
83+
84+
# keep reading from server
85+
terminated = False
86+
error_code = 0
87+
while not terminated:
88+
# read raw data from the socket
89+
data = tls_socket.recv(65536 * 1024)
90+
if not data:
91+
break
92+
93+
# feed raw data into h2, and process resulting events
94+
events = h2_connection.receive_data(data)
95+
for event in events:
96+
print(event)
97+
if isinstance(event, h2.events.ConnectionTerminated):
98+
if not event.error_code == 0:
99+
error_code = event.error_code
100+
terminated = True
101+
102+
# send any pending data to the server
103+
tls_socket.sendall(h2_connection.data_to_send())
104+
105+
# tell the server we are closing the h2 connection
106+
h2_connection.close_connection()
107+
tls_socket.sendall(h2_connection.data_to_send())
108+
109+
# close the socket
110+
tls_socket.close()
111+
112+
if error_code != 0:
113+
exit(1)
114+
115+
116+
def main():
117+
parser = argparse.ArgumentParser()
118+
parser.add_argument("port", type=int, help="Port to use")
119+
parser.add_argument("path", help="The path to request")
120+
parser.add_argument("-n", type=int, default=1, help="Number of streams to open")
121+
122+
args = parser.parse_args()
123+
124+
make_request(args.port, args.path, args.n)
125+
126+
127+
if __name__ == '__main__':
128+
main()
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
import sys
18+
19+
Test.Sumary = '''
20+
Verify Empty DATA Frame Handling
21+
'''
22+
23+
24+
class Http2EmptyDataFrameTest:
25+
26+
def __init__(self):
27+
self.__setupOriginServer()
28+
self.__setupTS()
29+
self.__setupClient()
30+
31+
def __setupOriginServer(self):
32+
self._server = Test.MakeHttpBinServer("httpbin")
33+
34+
def __setupTS(self):
35+
self._ts = Test.MakeATSProcess(f"ts", enable_tls=True, enable_cache=True)
36+
self._ts.addDefaultSSLFiles()
37+
self._ts.Disk.records_config.update(
38+
{
39+
'proxy.config.diags.debug.enabled': 1,
40+
'proxy.config.diags.debug.tags': 'http2',
41+
'proxy.config.ssl.server.cert.path': f"{self._ts.Variables.SSLDir}",
42+
'proxy.config.ssl.server.private_key.path': f"{self._ts.Variables.SSLDir}",
43+
'proxy.config.http.insert_response_via_str': 2,
44+
'proxy.config.http2.active_timeout_in': 3,
45+
'proxy.config.http2.stream_error_rate_threshold': 0.1 # default
46+
})
47+
self._ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self._server.Variables.Port}")
48+
self._ts.Disk.ssl_multicert_config.AddLine('dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key')
49+
50+
def __setupClient(self):
51+
self._ts.Setup.CopyAs("clients/h2empty_data_frame.py", Test.RunDirectory)
52+
53+
def run(self):
54+
tr = Test.AddTestRun("warm-up cache")
55+
56+
tr.Processes.Default.StartBefore(self._ts)
57+
tr.Processes.Default.StartBefore(self._server)
58+
59+
# warm up the cache
60+
tr.Processes.Default.Command = f"{sys.executable} h2empty_data_frame.py {self._ts.Variables.ssl_port} /cache/10 -n 1"
61+
tr.Processes.Default.ReturnCode = 0
62+
63+
# verify 20 streams doesn't hit `proxy.config.http2.stream_error_rate_threshold`
64+
tr = Test.AddTestRun("open 20 streams")
65+
tr.Processes.Default.Command = f"{sys.executable} h2empty_data_frame.py {self._ts.Variables.ssl_port} /cache/10 -n 20"
66+
tr.Processes.Default.ReturnCode = 0
67+
68+
tr.StillRunningAfter = self._ts
69+
70+
71+
Http2EmptyDataFrameTest().run()

0 commit comments

Comments
 (0)