Do not increase HTTP/3 MAX_STREAMS after sending a GOAWAY.

Protected by quic_reloadable_flag_quic_do_not_increase_max_streams_after_h3_goaway.

PiperOrigin-RevId: 571187342
diff --git a/quiche/quic/core/http/quic_spdy_session.cc b/quiche/quic/core/http/quic_spdy_session.cc
index 4a4bcd9..4789073 100644
--- a/quiche/quic/core/http/quic_spdy_session.cc
+++ b/quiche/quic/core/http/quic_spdy_session.cc
@@ -482,7 +482,9 @@
                               VersionUsesHttp3(transport_version())),
       force_buffer_requests_until_settings_(false),
       quic_enable_h3_datagrams_flag_(
-          GetQuicReloadableFlag(quic_enable_h3_datagrams)) {
+          GetQuicReloadableFlag(quic_enable_h3_datagrams)),
+      do_not_increase_max_streams_after_h3_goaway_flag_(GetQuicReloadableFlag(
+          quic_do_not_increase_max_streams_after_h3_goaway)) {
   h2_deframer_.set_visitor(spdy_framer_visitor_.get());
   h2_deframer_.set_debug_visitor(spdy_framer_visitor_.get());
   spdy_framer_.set_debug_visitor(spdy_framer_visitor_.get());
@@ -821,10 +823,14 @@
         ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
     return;
   }
-  QuicStreamId stream_id;
-
-  stream_id = QuicUtils::GetMaxClientInitiatedBidirectionalStreamId(
-      transport_version());
+  if (do_not_increase_max_streams_after_h3_goaway_flag_) {
+    QUICHE_RELOADABLE_FLAG_COUNT(
+        quic_do_not_increase_max_streams_after_h3_goaway);
+    ietf_streamid_manager().StopIncreasingIncomingMaxStreams();
+  }
+  QuicStreamId stream_id =
+      QuicUtils::GetMaxClientInitiatedBidirectionalStreamId(
+          transport_version());
   if (last_sent_http3_goaway_id_.has_value() &&
       last_sent_http3_goaway_id_.value() <= stream_id) {
     // Do not send GOAWAY frame with a higher id, because it is forbidden.
diff --git a/quiche/quic/core/http/quic_spdy_session.h b/quiche/quic/core/http/quic_spdy_session.h
index 9592614..9d21b5d 100644
--- a/quiche/quic/core/http/quic_spdy_session.h
+++ b/quiche/quic/core/http/quic_spdy_session.h
@@ -714,6 +714,10 @@
 
   // Latched value of quic_enable_h3_datagrams reloadable flag.
   bool quic_enable_h3_datagrams_flag_;
+
+  // Latched value of quic_do_not_increase_max_streams_after_h3_goaway
+  // reloadable flag.
+  bool do_not_increase_max_streams_after_h3_goaway_flag_;
 };
 
 }  // namespace quic
diff --git a/quiche/quic/core/http/quic_spdy_session_test.cc b/quiche/quic/core/http/quic_spdy_session_test.cc
index 0ccb217..2d368c2 100644
--- a/quiche/quic/core/http/quic_spdy_session_test.cc
+++ b/quiche/quic/core/http/quic_spdy_session_test.cc
@@ -1153,6 +1153,48 @@
   session_.SendHttp3GoAway(QUIC_PEER_GOING_AWAY, "Goaway");
 }
 
+TEST_P(QuicSpdySessionTestServer, SendHttp3GoAwayAndNoMoreMaxStreams) {
+  if (!VersionUsesHttp3(transport_version()) ||
+      !GetQuicReloadableFlag(
+          quic_do_not_increase_max_streams_after_h3_goaway)) {
+    return;
+  }
+
+  CompleteHandshake();
+  StrictMock<MockHttp3DebugVisitor> debug_visitor;
+  session_.set_debug_visitor(&debug_visitor);
+
+  EXPECT_CALL(*writer_, WritePacket(_, _, _, _, _, _))
+      .WillOnce(Return(WriteResult(WRITE_STATUS_OK, 0)));
+  // Send max stream id (currently 32 bits).
+  EXPECT_CALL(debug_visitor, OnGoAwayFrameSent(/* stream_id = */ 0xfffffffc));
+  session_.SendHttp3GoAway(QUIC_PEER_GOING_AWAY, "Goaway");
+  EXPECT_TRUE(session_.goaway_sent());
+
+  // No MAX_STREAMS frames should be sent, even after all available
+  // streams are opened and then closed.
+  EXPECT_CALL(*connection_, SendControlFrame(_)).Times(0);
+
+  const QuicStreamCount max_streams =
+      QuicSessionPeer::ietf_streamid_manager(&session_)
+          ->max_incoming_bidirectional_streams();
+  for (QuicStreamCount i = 0; i < max_streams; ++i) {
+    QuicStreamId stream_id = StreamCountToId(
+        i + 1,
+        Perspective::IS_CLIENT,  // Client initates stream, allocs stream id.
+        /*bidirectional=*/true);
+    EXPECT_NE(nullptr, session_.GetOrCreateStream(stream_id));
+
+    CloseStream(stream_id);
+    QuicRstStreamFrame rst_frame(kInvalidControlFrameId, stream_id,
+                                 QUIC_STREAM_CANCELLED,
+                                 /* bytes_written = */ 0);
+    session_.OnRstStream(rst_frame);
+  }
+  EXPECT_EQ(max_streams, QuicSessionPeer::ietf_streamid_manager(&session_)
+                             ->max_incoming_bidirectional_streams());
+}
+
 TEST_P(QuicSpdySessionTestServer, SendHttp3GoAwayWithoutEncryption) {
   if (!VersionUsesHttp3(transport_version())) {
     return;
diff --git a/quiche/quic/core/quic_flags_list.h b/quiche/quic/core/quic_flags_list.h
index 5f3b46b..e76cb60 100644
--- a/quiche/quic/core/quic_flags_list.h
+++ b/quiche/quic/core/quic_flags_list.h
@@ -77,6 +77,8 @@
 QUIC_FLAG(quic_reloadable_flag_quic_disable_batch_write, false)
 // If true, set burst token to 2 in cwnd bootstrapping experiment.
 QUIC_FLAG(quic_reloadable_flag_quic_conservative_bursts, false)
+// If true, then the HTTP/3 max streams limit will not be increased after sending a GOAWAY
+QUIC_FLAG(quic_reloadable_flag_quic_do_not_increase_max_streams_after_h3_goaway, true)
 // If true, use BBRv2 as the default congestion controller. Takes precedence over --quic_default_to_bbr.
 QUIC_FLAG(quic_reloadable_flag_quic_default_to_bbr_v2, false)
 // If true, use a LRU cache to record client addresses of packets received on server\'s original address.
diff --git a/quiche/quic/core/quic_session.h b/quiche/quic/core/quic_session.h
index 180d842..f347153 100644
--- a/quiche/quic/core/quic_session.h
+++ b/quiche/quic/core/quic_session.h
@@ -836,7 +836,7 @@
     connection()->SetLossDetectionTuner(std::move(tuner));
   }
 
-  const UberQuicStreamIdManager& ietf_streamid_manager() const {
+  UberQuicStreamIdManager& ietf_streamid_manager() {
     QUICHE_DCHECK(VersionHasIetfQuicFrames(transport_version()));
     return ietf_streamid_manager_;
   }
diff --git a/quiche/quic/core/uber_quic_stream_id_manager.cc b/quiche/quic/core/uber_quic_stream_id_manager.cc
index 2c8b1a5..c4b1cea 100644
--- a/quiche/quic/core/uber_quic_stream_id_manager.cc
+++ b/quiche/quic/core/uber_quic_stream_id_manager.cc
@@ -98,6 +98,11 @@
   return unidirectional_stream_id_manager_.IsAvailableStream(id);
 }
 
+void UberQuicStreamIdManager::StopIncreasingIncomingMaxStreams() {
+  unidirectional_stream_id_manager_.StopIncreasingIncomingMaxStreams();
+  bidirectional_stream_id_manager_.StopIncreasingIncomingMaxStreams();
+}
+
 QuicStreamCount
 UberQuicStreamIdManager::GetMaxAllowdIncomingBidirectionalStreams() const {
   return bidirectional_stream_id_manager_.incoming_initial_max_open_streams();
diff --git a/quiche/quic/core/uber_quic_stream_id_manager.h b/quiche/quic/core/uber_quic_stream_id_manager.h
index 7b836d4..8775851 100644
--- a/quiche/quic/core/uber_quic_stream_id_manager.h
+++ b/quiche/quic/core/uber_quic_stream_id_manager.h
@@ -68,6 +68,9 @@
   // Returns true if |id| is still available.
   bool IsAvailableStream(QuicStreamId id) const;
 
+  // Once called, the incoming max streams limit will never be increased.
+  void StopIncreasingIncomingMaxStreams();
+
   QuicStreamCount GetMaxAllowdIncomingBidirectionalStreams() const;
 
   QuicStreamCount GetMaxAllowdIncomingUnidirectionalStreams() const;