nghttpx: Add frontend-header-timeout option

Add frontend-header-timeout option.  frontend-read-timeout is
deprecated and has no effect.  Use frontend-header-timeout as a
replacement.  This also fixes the bug that HTTP/3 header timeout is
not set per stream.
This commit is contained in:
Tatsuhiro Tsujikawa
2024-03-16 11:19:44 +09:00
parent 566737ccc8
commit c8e149994e
10 changed files with 108 additions and 36 deletions

View File

@@ -201,6 +201,7 @@ OPTIONS = [
"require-http-scheme",
"tls-ktls",
"alpn-list",
"frontend-header-timeout",
]
LOGVARS = [

View File

@@ -2006,6 +2006,7 @@ void fill_default_config(Config *config) {
httpconf.xfp.add = true;
httpconf.xfp.strip_incoming = true;
httpconf.early_data.strip_incoming = true;
httpconf.timeout.header = 1_min;
auto &http2conf = config->http2;
{
@@ -2138,9 +2139,6 @@ void fill_default_config(Config *config) {
// Read timeout for HTTP3 upstream connection
timeoutconf.http3_read = 3_min;
// Read timeout for non-HTTP2 upstream connection
timeoutconf.read = 1_min;
// Write timeout for HTTP2/non-HTTP2 upstream connection
timeoutconf.write = 30_s;
@@ -2645,17 +2643,17 @@ Performance:
Timeout:
--frontend-http2-read-timeout=<DURATION>
Specify read timeout for HTTP/2 frontend connection.
Specify idle timeout for HTTP/2 frontend connection. If
no active streams exist for this duration, connection is
closed.
Default: )"
<< util::duration_str(config->conn.upstream.timeout.http2_read) << R"(
--frontend-http3-read-timeout=<DURATION>
Specify read timeout for HTTP/3 frontend connection.
Specify idle timeout for HTTP/3 frontend connection. If
no active streams exist for this duration, connection is
closed.
Default: )"
<< util::duration_str(config->conn.upstream.timeout.http3_read) << R"(
--frontend-read-timeout=<DURATION>
Specify read timeout for HTTP/1.1 frontend connection.
Default: )"
<< util::duration_str(config->conn.upstream.timeout.read) << R"(
--frontend-write-timeout=<DURATION>
Specify write timeout for all frontend connections.
Default: )"
@@ -2665,6 +2663,14 @@ Timeout:
connection.
Default: )"
<< util::duration_str(config->conn.upstream.timeout.idle_read) << R"(
--frontend-header-timeout=<DURATION>
Specify duration that the server waits for an HTTP
request header fields to be received completely. On
timeout, HTTP/1 and HTTP/2 connections are closed. For
HTTP/3, the stream is shutdown, and the connection
itself is left intact.
Default: )"
<< util::duration_str(config->http.timeout.header) << R"(
--stream-read-timeout=<DURATION>
Specify read timeout for HTTP/2 streams. 0 means no
timeout.
@@ -4377,6 +4383,8 @@ int main(int argc, char **argv) {
{SHRPX_OPT_REQUIRE_HTTP_SCHEME.c_str(), no_argument, &flag, 191},
{SHRPX_OPT_TLS_KTLS.c_str(), no_argument, &flag, 192},
{SHRPX_OPT_ALPN_LIST.c_str(), required_argument, &flag, 193},
{SHRPX_OPT_FRONTEND_HEADER_TIMEOUT.c_str(), required_argument, &flag,
194},
{nullptr, 0, nullptr, 0}};
int option_index = 0;
@@ -5294,6 +5302,11 @@ int main(int argc, char **argv) {
// --alpn-list
cmdcfgs.emplace_back(SHRPX_OPT_ALPN_LIST, StringRef{optarg});
break;
case 194:
// --frontend-header-timeout
cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_HEADER_TIMEOUT,
StringRef{optarg});
break;
default:
break;
}

View File

@@ -444,7 +444,7 @@ ClientHandler::ClientHandler(Worker *worker, int fd, SSL *ssl,
rb_(worker->get_mcpool()),
conn_(worker->get_loop(), fd, ssl, worker->get_mcpool(),
get_config()->conn.upstream.timeout.write,
get_config()->conn.upstream.timeout.read,
get_config()->conn.upstream.timeout.idle_read,
get_config()->conn.upstream.ratelimit.write,
get_config()->conn.upstream.ratelimit.read, writecb, readcb,
timeoutcb, this, get_config()->tls.dyn_rec.warmup_threshold,
@@ -591,16 +591,14 @@ struct ev_loop *ClientHandler::get_loop() const { return conn_.loop; }
void ClientHandler::reset_upstream_read_timeout(ev_tstamp t) {
conn_.rt.repeat = t;
if (ev_is_active(&conn_.rt)) {
ev_timer_again(conn_.loop, &conn_.rt);
}
ev_timer_again(conn_.loop, &conn_.rt);
}
void ClientHandler::reset_upstream_write_timeout(ev_tstamp t) {
conn_.wt.repeat = t;
if (ev_is_active(&conn_.wt)) {
ev_timer_again(conn_.loop, &conn_.wt);
}
ev_timer_again(conn_.loop, &conn_.wt);
}
void ClientHandler::repeat_read_timer() {

View File

@@ -2396,6 +2396,9 @@ int option_lookup_token(const char *name, size_t namelen) {
if (util::strieq_l("backend-connect-timeou", name, 22)) {
return SHRPX_OPTID_BACKEND_CONNECT_TIMEOUT;
}
if (util::strieq_l("frontend-header-timeou", name, 22)) {
return SHRPX_OPTID_FRONTEND_HEADER_TIMEOUT;
}
break;
}
break;
@@ -3031,7 +3034,9 @@ int parse_config(Config *config, int optid, const StringRef &opt,
return parse_duration(&config->conn.upstream.timeout.http2_read, opt,
optarg);
case SHRPX_OPTID_FRONTEND_READ_TIMEOUT:
return parse_duration(&config->conn.upstream.timeout.read, opt, optarg);
LOG(WARN) << opt << ": deprecated. Use frontend-header-timeout";
return 0;
case SHRPX_OPTID_FRONTEND_WRITE_TIMEOUT:
return parse_duration(&config->conn.upstream.timeout.write, opt, optarg);
case SHRPX_OPTID_BACKEND_READ_TIMEOUT:

View File

@@ -406,6 +406,8 @@ constexpr auto SHRPX_OPT_REQUIRE_HTTP_SCHEME =
StringRef::from_lit("require-http-scheme");
constexpr auto SHRPX_OPT_TLS_KTLS = StringRef::from_lit("tls-ktls");
constexpr auto SHRPX_OPT_ALPN_LIST = StringRef::from_lit("alpn-list");
constexpr auto SHRPX_OPT_FRONTEND_HEADER_TIMEOUT =
StringRef::from_lit("frontend-header-timeout");
constexpr size_t SHRPX_OBFUSCATED_NODE_LENGTH = 8;
@@ -864,6 +866,9 @@ struct HttpConfig {
struct {
bool strip_incoming;
} early_data;
struct {
ev_tstamp header;
} timeout;
std::vector<AltSvc> altsvcs;
// altsvcs serialized in a wire format.
StringRef altsvc_header_value;
@@ -1050,7 +1055,6 @@ struct ConnectionConfig {
struct {
ev_tstamp http2_read;
ev_tstamp http3_read;
ev_tstamp read;
ev_tstamp write;
ev_tstamp idle_read;
} timeout;
@@ -1249,6 +1253,7 @@ enum {
SHRPX_OPTID_FORWARDED_FOR,
SHRPX_OPTID_FRONTEND,
SHRPX_OPTID_FRONTEND_FRAME_DEBUG,
SHRPX_OPTID_FRONTEND_HEADER_TIMEOUT,
SHRPX_OPTID_FRONTEND_HTTP2_CONNECTION_WINDOW_BITS,
SHRPX_OPTID_FRONTEND_HTTP2_CONNECTION_WINDOW_SIZE,
SHRPX_OPTID_FRONTEND_HTTP2_DECODER_DYNAMIC_TABLE_SIZE,

View File

@@ -45,6 +45,23 @@
namespace shrpx {
namespace {
void header_timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) {
auto downstream = static_cast<Downstream *>(w->data);
auto upstream = downstream->get_upstream();
if (LOG_ENABLED(INFO)) {
DLOG(INFO, downstream) << "request header timeout stream_id="
<< downstream->get_stream_id();
}
downstream->disable_upstream_rtimer();
downstream->disable_upstream_wtimer();
upstream->on_timeout(downstream);
}
} // namespace
namespace {
void upstream_timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) {
auto downstream = static_cast<Downstream *>(w->data);
@@ -148,7 +165,12 @@ Downstream::Downstream(Upstream *upstream, MemchunkPool *mcpool,
expect_100_continue_(false),
stop_reading_(false) {
auto &timeoutconf = get_config()->http2.timeout;
auto config = get_config();
auto &httpconf = config->http;
ev_timer_init(&header_timer_, header_timeoutcb, 0., httpconf.timeout.header);
auto &timeoutconf = config->http2.timeout;
ev_timer_init(&upstream_rtimer_, &upstream_rtimeoutcb, 0.,
timeoutconf.stream_read);
@@ -159,6 +181,7 @@ Downstream::Downstream(Upstream *upstream, MemchunkPool *mcpool,
ev_timer_init(&downstream_wtimer_, &downstream_wtimeoutcb, 0.,
timeoutconf.stream_write);
header_timer_.data = this;
upstream_rtimer_.data = this;
upstream_wtimer_.data = this;
downstream_rtimer_.data = this;
@@ -183,6 +206,7 @@ Downstream::~Downstream() {
ev_timer_stop(loop, &upstream_wtimer_);
ev_timer_stop(loop, &downstream_rtimer_);
ev_timer_stop(loop, &downstream_wtimer_);
ev_timer_stop(loop, &header_timer_);
#ifdef HAVE_MRUBY
auto handler = upstream_->get_client_handler();
@@ -946,6 +970,18 @@ bool Downstream::expect_response_trailer() const {
(resp_.http_major == 3 || resp_.http_major == 2);
}
void Downstream::repeat_header_timer() {
auto loop = upstream_->get_client_handler()->get_loop();
ev_timer_again(loop, &header_timer_);
}
void Downstream::stop_header_timer() {
auto loop = upstream_->get_client_handler()->get_loop();
ev_timer_stop(loop, &header_timer_);
}
namespace {
void reset_timer(struct ev_loop *loop, ev_timer *w) { ev_timer_again(loop, w); }
} // namespace

View File

@@ -448,6 +448,9 @@ public:
// connection.
int on_read();
void repeat_header_timer();
void stop_header_timer();
// Resets upstream read timer. If it is active, timeout value is
// reset. If it is not active, timer will be started.
void reset_upstream_rtimer();
@@ -562,6 +565,8 @@ private:
// if frontend uses RFC 8441 WebSocket bootstrapping via HTTP/2.
StringRef ws_key_;
ev_timer header_timer_;
ev_timer upstream_rtimer_;
ev_timer upstream_wtimer_;

View File

@@ -285,7 +285,10 @@ void Http2Upstream::on_start_request(const nghttp2_frame *frame) {
downstream->reset_upstream_rtimer();
handler_->repeat_read_timer();
auto config = get_config();
auto &httpconf = config->http;
handler_->reset_upstream_read_timeout(httpconf.timeout.header);
auto &req = downstream->request();
@@ -298,8 +301,6 @@ void Http2Upstream::on_start_request(const nghttp2_frame *frame) {
++num_requests_;
auto config = get_config();
auto &httpconf = config->http;
if (httpconf.max_requests <= num_requests_) {
start_graceful_shutdown();
}
@@ -1640,7 +1641,10 @@ void Http2Upstream::remove_downstream(Downstream *downstream) {
if (downstream_queue_.get_downstreams() == nullptr) {
// There is no downstream at the moment. Start idle timer now.
handler_->repeat_read_timer();
auto config = get_config();
auto &upstreamconf = config->conn.upstream;
handler_->reset_upstream_read_timeout(upstreamconf.timeout.http2_read);
}
}

View File

@@ -250,8 +250,9 @@ void Http3Upstream::http_begin_request_headers(int64_t stream_id) {
nghttp3_conn_set_stream_user_data(httpconn_, stream_id, downstream.get());
downstream->reset_upstream_rtimer();
downstream->repeat_header_timer();
handler_->repeat_read_timer();
handler_->stop_read_timer();
auto &req = downstream->request();
req.http_major = 3;
@@ -997,7 +998,18 @@ int Http3Upstream::write_streams() {
return 0;
}
int Http3Upstream::on_timeout(Downstream *downstream) { return 0; }
int Http3Upstream::on_timeout(Downstream *downstream) {
if (LOG_ENABLED(INFO)) {
ULOG(INFO, this) << "Stream timeout stream_id="
<< downstream->get_stream_id();
}
shutdown_stream(downstream, NGHTTP3_H3_INTERNAL_ERROR);
handler_->signal_write();
return 0;
}
int Http3Upstream::on_downstream_abort_request(Downstream *downstream,
unsigned int status_code) {
@@ -2187,7 +2199,6 @@ namespace {
int http_end_request_headers(nghttp3_conn *conn, int64_t stream_id, int fin,
void *user_data, void *stream_user_data) {
auto upstream = static_cast<Http3Upstream *>(user_data);
auto handler = upstream->get_client_handler();
auto downstream = static_cast<Downstream *>(stream_user_data);
if (!downstream || downstream->get_stop_reading()) {
@@ -2199,7 +2210,7 @@ int http_end_request_headers(nghttp3_conn *conn, int64_t stream_id, int fin,
}
downstream->reset_upstream_rtimer();
handler->stop_read_timer();
downstream->stop_header_timer();
return 0;
}

View File

@@ -115,12 +115,9 @@ void HttpsUpstream::on_start_request() {
attach_downstream(std::move(downstream));
auto conn = handler_->get_connection();
auto &upstreamconf = get_config()->conn.upstream;
auto &httpconf = get_config()->http;
conn->rt.repeat = upstreamconf.timeout.read;
handler_->repeat_read_timer();
handler_->reset_upstream_read_timeout(httpconf.timeout.header);
++num_requests_;
}
@@ -795,12 +792,9 @@ int HttpsUpstream::on_write() {
return 0;
}
auto conn = handler_->get_connection();
auto &upstreamconf = get_config()->conn.upstream;
conn->rt.repeat = upstreamconf.timeout.idle_read;
handler_->repeat_read_timer();
handler_->reset_upstream_read_timeout(upstreamconf.timeout.idle_read);
return resume_read(SHRPX_NO_BUFFER, nullptr, 0);
} else {