Expose the number of available peer-issued CIDs

This allows Chromium code to record the number of available CIDs whenever it attempts to migration. This is going to be useful to validate:
1. That the CID capacity experiment is working
2. The ideal number of spare CID GFEs should issue

See https://crrev.com/c/8483300.

PiperOrigin-RevId: 991740308
diff --git a/quiche/quic/core/quic_connection.cc b/quiche/quic/core/quic_connection.cc
index 841df24..2ffa025 100644
--- a/quiche/quic/core/quic_connection.cc
+++ b/quiche/quic/core/quic_connection.cc
@@ -4413,6 +4413,12 @@
           peer_issued_cid_manager_->HasUnusedConnectionId());
 }
 
+size_t QuicConnection::NumUnusedPeerIssuedConnectionIds() const {
+  return peer_issued_cid_manager_ == nullptr
+             ? 0
+             : peer_issued_cid_manager_->NumUnusedConnectionIds();
+}
+
 void QuicConnection::OnRetransmissionAlarm() {
   QUICHE_DCHECK(connected());
   ScopedRetransmissionTimeoutIndicator indicator(this);
diff --git a/quiche/quic/core/quic_connection.h b/quiche/quic/core/quic_connection.h
index d59463d..442df61 100644
--- a/quiche/quic/core/quic_connection.h
+++ b/quiche/quic/core/quic_connection.h
@@ -1364,6 +1364,9 @@
 
   bool HasUnusedConnectionId() const;
 
+  // Returns the number of unused peer-issued connection IDs.
+  size_t NumUnusedPeerIssuedConnectionIds() const;
+
   QuicPathValidationContext* GetPathValidationContext() const;
 
   // TODO(martinduke): Delete this deprecated method once non-QUICHE callers are
diff --git a/quiche/quic/core/quic_connection_id_manager.h b/quiche/quic/core/quic_connection_id_manager.h
index a466fb1..5276eca 100644
--- a/quiche/quic/core/quic_connection_id_manager.h
+++ b/quiche/quic/core/quic_connection_id_manager.h
@@ -75,6 +75,10 @@
     return !unused_connection_id_data_.empty();
   }
 
+  size_t NumUnusedConnectionIds() const {
+    return unused_connection_id_data_.size();
+  }
+
   // Returns the data associated with an unused connection Id. After the call,
   // the Id is marked as used. Returns nullptr if there is no unused connection
   // Id.
diff --git a/quiche/quic/core/quic_connection_id_manager_test.cc b/quiche/quic/core/quic_connection_id_manager_test.cc
index 03af309..9f6a2da 100644
--- a/quiche/quic/core/quic_connection_id_manager_test.cc
+++ b/quiche/quic/core/quic_connection_id_manager_test.cc
@@ -110,6 +110,49 @@
   bool duplicate_frame_ = false;
 };
 
+TEST_F(QuicPeerIssuedConnectionIdManagerTest, NumUnusedConnectionIds) {
+  EXPECT_EQ(0u, peer_issued_cid_manager_.NumUnusedConnectionIds());
+  EXPECT_FALSE(peer_issued_cid_manager_.HasUnusedConnectionId());
+
+  // Receives CID #1 from peer.
+  QuicNewConnectionIdFrame frame1;
+  frame1.connection_id = TestConnectionId(1);
+  frame1.sequence_number = 1u;
+  frame1.retire_prior_to = 0u;
+  frame1.stateless_reset_token =
+      QuicUtils::GenerateStatelessResetToken(frame1.connection_id);
+  ASSERT_THAT(peer_issued_cid_manager_.OnNewConnectionIdFrame(
+                  frame1, &error_details_, &duplicate_frame_),
+              IsQuicNoError());
+  EXPECT_EQ(1u, peer_issued_cid_manager_.NumUnusedConnectionIds());
+  EXPECT_TRUE(peer_issued_cid_manager_.HasUnusedConnectionId());
+
+  // Receives CID #2 from peer.
+  peer_issued_cid_manager_.set_active_connection_id_limit(3);
+  QuicNewConnectionIdFrame frame2;
+  frame2.connection_id = TestConnectionId(2);
+  frame2.sequence_number = 2u;
+  frame2.retire_prior_to = 0u;
+  frame2.stateless_reset_token =
+      QuicUtils::GenerateStatelessResetToken(frame2.connection_id);
+  ASSERT_THAT(peer_issued_cid_manager_.OnNewConnectionIdFrame(
+                  frame2, &error_details_, &duplicate_frame_),
+              IsQuicNoError());
+  EXPECT_EQ(2u, peer_issued_cid_manager_.NumUnusedConnectionIds());
+
+  // Consume one unused connection ID.
+  const QuicConnectionIdData* cid_data =
+      peer_issued_cid_manager_.ConsumeOneUnusedConnectionId();
+  ASSERT_THAT(cid_data, testing::NotNull());
+  EXPECT_EQ(1u, peer_issued_cid_manager_.NumUnusedConnectionIds());
+
+  // Consume the remaining unused connection ID.
+  cid_data = peer_issued_cid_manager_.ConsumeOneUnusedConnectionId();
+  ASSERT_THAT(cid_data, testing::NotNull());
+  EXPECT_EQ(0u, peer_issued_cid_manager_.NumUnusedConnectionIds());
+  EXPECT_FALSE(peer_issued_cid_manager_.HasUnusedConnectionId());
+}
+
 TEST_F(QuicPeerIssuedConnectionIdManagerTest,
        ConnectionIdSequenceWhenMigrationSucceed) {
   {
diff --git a/quiche/quic/core/quic_connection_test.cc b/quiche/quic/core/quic_connection_test.cc
index 7c710c9..72258ed 100644
--- a/quiche/quic/core/quic_connection_test.cc
+++ b/quiche/quic/core/quic_connection_test.cc
@@ -13642,6 +13642,39 @@
   }
 }
 
+TEST_P(QuicConnectionTest, NumUnusedPeerIssuedConnectionIds) {
+  if (!version().IsIetfQuic()) {
+    EXPECT_EQ(0u, connection_.NumUnusedPeerIssuedConnectionIds());
+    return;
+  }
+  EXPECT_EQ(0u, connection_.NumUnusedPeerIssuedConnectionIds());
+
+  connection_.CreateConnectionIdManager();
+  EXPECT_EQ(0u, connection_.NumUnusedPeerIssuedConnectionIds());
+
+  connection_.SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE);
+  connection_.OnHandshakeComplete();
+  EXPECT_CALL(visitor_, GetHandshakeState())
+      .WillRepeatedly(Return(HANDSHAKE_CONFIRMED));
+
+  QuicNewConnectionIdFrame frame;
+  frame.connection_id = TestConnectionId(1234);
+  ASSERT_NE(frame.connection_id, connection_.connection_id());
+  frame.stateless_reset_token =
+      QuicUtils::GenerateStatelessResetToken(frame.connection_id);
+  frame.retire_prior_to = 0u;
+  frame.sequence_number = 1u;
+  EXPECT_TRUE(connection_.OnNewConnectionIdFrame(frame));
+  EXPECT_EQ(1u, connection_.NumUnusedPeerIssuedConnectionIds());
+
+  const QuicSocketAddress kNewSelfAddress(QuicIpAddress::Any4(), 12345);
+  TestPacketWriter new_writer(version(), &clock_, Perspective::IS_CLIENT);
+  EXPECT_TRUE(connection_.MigratePath(kNewSelfAddress,
+                                      connection_.peer_address(), &new_writer,
+                                      /*owns_writer=*/false));
+  EXPECT_EQ(0u, connection_.NumUnusedPeerIssuedConnectionIds());
+}
+
 TEST_P(QuicConnectionTest, MigrateToNewPathDuringProbing) {
   if (!VersionIsIetfQuic(connection_.version().transport_version)) {
     return;