Set the failure reason before reporting a failure when no CID is available

ValidatePath is not setting a failure reason before failing due to lack of unused connection IDs. This leads to OnPathValidationFailure being called with a nullopt reason, making it impossible for embedders to identify and prioritize connection migration failures.

Similarly, there is currently no reason for embedders to use when canceling a path validation due to a writer error. Add a new kWriterError to cover this scenario.

See https://crrev.com/c/8444838 for the Chromium changes that will land once QUICHE is rolled there. This should make the kProbeFailed/"Probe failed" bucket for the new Net.Quic.Migration.Attempt.FailureReason UMA actionable.

Finally, mark kUnknown as deprecated: an actual failure reason should always be used.

PiperOrigin-RevId: 988257046
diff --git a/quiche/quic/core/quic_connection.cc b/quiche/quic/core/quic_connection.cc
index 7553af5..beda35d 100644
--- a/quiche/quic/core/quic_connection.cc
+++ b/quiche/quic/core/quic_connection.cc
@@ -6951,7 +6951,9 @@
     }
     if (!HasUnusedConnectionId()) {
       QUIC_DVLOG(1) << "Client cannot start new path validation as there is no "
-                       "requried connection ID is available.";
+                       "unused connection ID available.";
+      context->set_failure_reason(
+          PathValidationFailure::Reason::kNoAvailableConnectionId);
       result_delegate->OnPathValidationFailure(std::move(context));
       return;
     }
diff --git a/quiche/quic/core/quic_connection_test.cc b/quiche/quic/core/quic_connection_test.cc
index b79f9fb..58ae0f1 100644
--- a/quiche/quic/core/quic_connection_test.cc
+++ b/quiche/quic/core/quic_connection_test.cc
@@ -12103,11 +12103,13 @@
                   new_writer.last_write_source_address());
       });
   bool success = true;
+  std::optional<PathValidationFailure::Reason> failure_reason;
   connection_.ValidatePath(
       std::make_unique<TestQuicPathValidationContext>(
           kNewSelfAddress, connection_.peer_address(), &new_writer),
       std::make_unique<TestValidationResultDelegate>(
-          &connection_, kNewSelfAddress, connection_.peer_address(), &success),
+          &connection_, kNewSelfAddress, connection_.peer_address(), &success,
+          &failure_reason),
       PathValidationReason::kReasonUnknown);
   EXPECT_EQ(0u, writer_->packets_write_attempts());
 
@@ -12116,15 +12118,20 @@
   EXPECT_NE(kNewSelfAddress2, connection_.self_address());
   TestPacketWriter new_writer2(version(), &clock_, Perspective::IS_CLIENT);
   bool success2 = false;
+  std::optional<PathValidationFailure::Reason> failure_reason2;
   connection_.ValidatePath(
       std::make_unique<TestQuicPathValidationContext>(
           kNewSelfAddress2, connection_.peer_address(), &new_writer2),
       std::make_unique<TestValidationResultDelegate>(
-          &connection_, kNewSelfAddress2, connection_.peer_address(),
-          &success2),
+          &connection_, kNewSelfAddress2, connection_.peer_address(), &success2,
+          &failure_reason2),
       PathValidationReason::kReasonUnknown);
   EXPECT_FALSE(success);
-  // There is no pening path validation as there is no available connection ID.
+  EXPECT_EQ(failure_reason, PathValidationFailure::Reason::kNewerValidation);
+  EXPECT_FALSE(success2);
+  EXPECT_EQ(failure_reason2,
+            PathValidationFailure::Reason::kNoAvailableConnectionId);
+  // There is no pending path validation as there is no available connection ID.
   EXPECT_FALSE(connection_.HasPendingPathValidation());
 }
 
diff --git a/quiche/quic/core/quic_path_validator.h b/quiche/quic/core/quic_path_validator.h
index 6f0a3d6..5881163 100644
--- a/quiche/quic/core/quic_path_validator.h
+++ b/quiche/quic/core/quic_path_validator.h
@@ -45,12 +45,23 @@
 
 struct QUICHE_EXPORT PathValidationFailure {
   enum class Reason : uint8_t {
+    // Do not use this. Instead, create a new enum value specific to the new
+    // failure scenario.
     kUnknown = 0,
-    kStatelessReset = 1,   // The peer sent a stateless reset on the path.
-    kNewerValidation = 2,  // Starting validation on a new path.
-    kRetryTimeout = 3,     // The validation process hit the retry limit.
-    kNotConnected = 4,     // PATH_CHALLENGE can't be sent because the
-                           // connection is no longer active.
+    // The peer sent a stateless reset on the path.
+    kStatelessReset = 1,
+    // Starting validation on a new path.
+    kNewerValidation = 2,
+    // The validation process hit the retry limit.
+    kRetryTimeout = 3,
+    // PATH_CHALLENGE can't be sent because the connection is no longer active.
+    kNotConnected = 4,
+    // No unused connection ID was available, so validation could not start.
+    // No PATH_CHALLENGE was ever sent on this path.
+    kNoAvailableConnectionId = 5,
+    // Writing to the path failed. The writer belongs to the caller, so this is
+    // normally reported by the caller while cancelling its own validation.
+    kWriterError = 6,
   };
   Reason reason;
 };