Do not evict unacknowledged QPACK entries.
See https://crbug.com/1441880.
Protected by FLAGS_quic_reloadable_flag_quic_do_not_evict_unacked_entry.
PiperOrigin-RevId: 529092490
diff --git a/quiche/quic/core/qpack/qpack_encoder.cc b/quiche/quic/core/qpack/qpack_encoder.cc
index 95e55b4..7ac9519 100644
--- a/quiche/quic/core/qpack/qpack_encoder.cc
+++ b/quiche/quic/core/qpack/qpack_encoder.cc
@@ -13,6 +13,8 @@
#include "quiche/quic/core/qpack/qpack_instruction_encoder.h"
#include "quiche/quic/core/qpack/qpack_required_insert_count.h"
#include "quiche/quic/core/qpack/value_splitting_header_list.h"
+#include "quiche/quic/platform/api/quic_flag_utils.h"
+#include "quiche/quic/platform/api/quic_flags.h"
#include "quiche/quic/platform/api/quic_logging.h"
namespace quic {
@@ -85,13 +87,23 @@
Representations representations;
representations.reserve(header_list.size());
- // The index of the oldest entry that must not be evicted.
- uint64_t smallest_blocking_index =
- blocking_manager_.smallest_blocking_index();
// Entries with index larger than or equal to |known_received_count| are
// blocking.
const uint64_t known_received_count =
blocking_manager_.known_received_count();
+
+ // The index of the oldest entry that must not be evicted. Blocking entries
+ // must not be evicted.
+ uint64_t smallest_non_evictable_index =
+ blocking_manager_.smallest_blocking_index();
+ // Additionally, unacknowledged entries must not be evicted, even if they have
+ // no outstanding references (see https://crbug.com/1441880 for more context).
+ if (GetQuicReloadableFlag(quic_do_not_evict_unacked_entry)) {
+ QUIC_RELOADABLE_FLAG_COUNT(quic_do_not_evict_unacked_entry);
+ smallest_non_evictable_index =
+ std::min(smallest_non_evictable_index, known_received_count);
+ }
+
// Only entries with index greater than or equal to |draining_index| are
// allowed to be referenced.
const uint64_t draining_index =
@@ -133,7 +145,8 @@
} else {
representations.push_back(
EncodeIndexedHeaderField(is_static, index, referred_indices));
- smallest_blocking_index = std::min(smallest_blocking_index, index);
+ smallest_non_evictable_index =
+ std::min(smallest_non_evictable_index, index);
header_table_.set_dynamic_table_entry_referenced();
break;
@@ -144,7 +157,7 @@
blocked_stream_limit_exhausted = true;
} else if (QpackEntry::Size(name, value) >
header_table_.MaxInsertSizeWithoutEvictingGivenEntry(
- std::min(smallest_blocking_index, index))) {
+ std::min(smallest_non_evictable_index, index))) {
dynamic_table_insertion_blocked = true;
} else {
if (can_write_to_encoder_stream) {
@@ -155,8 +168,8 @@
uint64_t new_index = header_table_.InsertEntry(name, value);
representations.push_back(EncodeIndexedHeaderField(
is_static, new_index, referred_indices));
- smallest_blocking_index =
- std::min(smallest_blocking_index, index);
+ smallest_non_evictable_index =
+ std::min(smallest_non_evictable_index, index);
header_table_.set_dynamic_table_entry_referenced();
break;
@@ -178,7 +191,7 @@
if (blocking_allowed &&
QpackEntry::Size(name, value) <=
header_table_.MaxInsertSizeWithoutEvictingGivenEntry(
- smallest_blocking_index)) {
+ smallest_non_evictable_index)) {
// If allowed, insert entry into dynamic table and refer to it.
if (can_write_to_encoder_stream) {
encoder_stream_sender_.SendInsertWithNameReference(is_static,
@@ -186,8 +199,8 @@
uint64_t new_index = header_table_.InsertEntry(name, value);
representations.push_back(EncodeIndexedHeaderField(
/* is_static = */ false, new_index, referred_indices));
- smallest_blocking_index =
- std::min<uint64_t>(smallest_blocking_index, new_index);
+ smallest_non_evictable_index =
+ std::min<uint64_t>(smallest_non_evictable_index, new_index);
break;
}
@@ -204,7 +217,7 @@
blocked_stream_limit_exhausted = true;
} else if (QpackEntry::Size(name, value) >
header_table_.MaxInsertSizeWithoutEvictingGivenEntry(
- std::min(smallest_blocking_index, index))) {
+ std::min(smallest_non_evictable_index, index))) {
dynamic_table_insertion_blocked = true;
} else {
// If allowed, insert entry with name reference and refer to it.
@@ -217,7 +230,8 @@
uint64_t new_index = header_table_.InsertEntry(name, value);
representations.push_back(EncodeIndexedHeaderField(
is_static, new_index, referred_indices));
- smallest_blocking_index = std::min(smallest_blocking_index, index);
+ smallest_non_evictable_index =
+ std::min(smallest_non_evictable_index, index);
header_table_.set_dynamic_table_entry_referenced();
break;
@@ -229,7 +243,8 @@
// If allowed, refer to entry name directly, with literal value.
representations.push_back(EncodeLiteralHeaderFieldWithNameReference(
is_static, index, value, referred_indices));
- smallest_blocking_index = std::min(smallest_blocking_index, index);
+ smallest_non_evictable_index =
+ std::min(smallest_non_evictable_index, index);
header_table_.set_dynamic_table_entry_referenced();
break;
@@ -250,7 +265,7 @@
blocked_stream_limit_exhausted = true;
} else if (QpackEntry::Size(name, value) >
header_table_.MaxInsertSizeWithoutEvictingGivenEntry(
- smallest_blocking_index)) {
+ smallest_non_evictable_index)) {
dynamic_table_insertion_blocked = true;
} else {
if (can_write_to_encoder_stream) {
@@ -258,8 +273,8 @@
uint64_t new_index = header_table_.InsertEntry(name, value);
representations.push_back(EncodeIndexedHeaderField(
/* is_static = */ false, new_index, referred_indices));
- smallest_blocking_index =
- std::min<uint64_t>(smallest_blocking_index, new_index);
+ smallest_non_evictable_index =
+ std::min<uint64_t>(smallest_non_evictable_index, new_index);
break;
}
@@ -268,10 +283,7 @@
// Encode entry as string literals.
// TODO(b/112770235): Consider also adding to dynamic table to improve
// compression ratio for subsequent header blocks with peers that do not
- // allow any blocked streams. However, RFC 9204 Section 2.1.1 forbids
- // evicting an unacknowledged entry even if it has no outstanding
- // references, so `smallest_blocking_index` would need to be updated to
- // consider that.
+ // allow any blocked streams.
representations.push_back(EncodeLiteralHeaderField(name, value));
break;
diff --git a/quiche/quic/core/qpack/qpack_encoder_test.cc b/quiche/quic/core/qpack/qpack_encoder_test.cc
index aa40a8a..0e59048 100644
--- a/quiche/quic/core/qpack/qpack_encoder_test.cc
+++ b/quiche/quic/core/qpack/qpack_encoder_test.cc
@@ -10,6 +10,7 @@
#include "absl/strings/escaping.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
+#include "quiche/quic/platform/api/quic_flags.h"
#include "quiche/quic/platform/api/quic_test.h"
#include "quiche/quic/test_tools/qpack/qpack_encoder_peer.h"
#include "quiche/quic/test_tools/qpack/qpack_test_utils.h"
@@ -628,6 +629,81 @@
EXPECT_EQ(0u, encoder_stream_sent_byte_count_);
}
+// Regression test for https://crbug.com/1441880.
+TEST_F(QpackEncoderTest, UnackedEntryCannotBeEvicted) {
+ EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
+ .WillRepeatedly(Return(0));
+ encoder_.SetMaximumBlockedStreams(2);
+ // With 32 byte overhead per entry, only one entry fits in the dynamic table.
+ encoder_.SetMaximumDynamicTableCapacity(40);
+ encoder_.SetDynamicTableCapacity(40);
+
+ QpackEncoderHeaderTable* header_table =
+ QpackEncoderPeer::header_table(&encoder_);
+ EXPECT_EQ(0u, header_table->inserted_entry_count());
+ EXPECT_EQ(0u, header_table->dropped_entry_count());
+
+ spdy::Http2HeaderBlock header_list1;
+ header_list1["foo"] = "bar";
+
+ // Set Dynamic Table Capacity instruction.
+ std::string set_dyanamic_table_capacity = absl::HexStringToBytes("3f09");
+ // Insert one entry into the dynamic table.
+ std::string insert_entries1 = absl::HexStringToBytes(
+ "62" // insert without name reference
+ "94e7" // Huffman-encoded name "foo"
+ "03626172"); // value "bar"
+ EXPECT_CALL(encoder_stream_sender_delegate_,
+ WriteStreamData(Eq(
+ absl::StrCat(set_dyanamic_table_capacity, insert_entries1))));
+
+ EXPECT_EQ(
+ absl::HexStringToBytes("0200" // prefix
+ "80"), // dynamic entry with relative index 0
+ encoder_.EncodeHeaderList(/* stream_id = */ 1, header_list1,
+ &encoder_stream_sent_byte_count_));
+
+ EXPECT_EQ(1u, header_table->inserted_entry_count());
+ EXPECT_EQ(0u, header_table->dropped_entry_count());
+
+ encoder_.OnStreamCancellation(/* stream_id = */ 1);
+
+ // At this point, entry 0 has no references to it, because stream 1 is
+ // cancelled. However, this entry is unacknowledged, therefore it must not be
+ // evicted according to RFC 9204 Section 2.1.1.
+
+ spdy::Http2HeaderBlock header_list2;
+ header_list2["bar"] = "baz";
+
+ if (GetQuicReloadableFlag(quic_do_not_evict_unacked_entry)) {
+ EXPECT_EQ(absl::HexStringToBytes("0000" // prefix
+ "23626172" // literal name "bar"
+ "0362617a"), // literal value "baz"
+ encoder_.EncodeHeaderList(/* stream_id = */ 2, header_list2,
+ &encoder_stream_sent_byte_count_));
+
+ EXPECT_EQ(1u, header_table->inserted_entry_count());
+ EXPECT_EQ(0u, header_table->dropped_entry_count());
+ } else {
+ // Insert one entry into the dynamic table, evicting the first entry.
+ std::string insert_entries2 = absl::HexStringToBytes(
+ "43" // insert without name reference
+ "626172" // name "bar"
+ "0362617a"); // value "baz"
+ EXPECT_CALL(encoder_stream_sender_delegate_,
+ WriteStreamData(Eq(insert_entries2)));
+
+ EXPECT_EQ(
+ absl::HexStringToBytes("0100" // prefix
+ "80"), // dynamic entry with relative index 0
+ encoder_.EncodeHeaderList(/* stream_id = */ 2, header_list2,
+ &encoder_stream_sent_byte_count_));
+
+ EXPECT_EQ(2u, header_table->inserted_entry_count());
+ EXPECT_EQ(1u, header_table->dropped_entry_count());
+ }
+}
+
} // namespace
} // namespace test
} // namespace quic
diff --git a/quiche/quic/core/quic_flags_list.h b/quiche/quic/core/quic_flags_list.h
index 75d63ad..5023083 100644
--- a/quiche/quic/core/quic_flags_list.h
+++ b/quiche/quic/core/quic_flags_list.h
@@ -67,6 +67,8 @@
QUIC_FLAG(quic_restart_flag_quic_enable_sending_bandwidth_estimate_when_network_idle_v2, true)
// If true, set burst token to 2 in cwnd bootstrapping experiment.
QUIC_FLAG(quic_reloadable_flag_quic_conservative_bursts, false)
+// If true, unacked QPACK entries with no references will not be evicted.
+QUIC_FLAG(quic_reloadable_flag_quic_do_not_evict_unacked_entry, false)
// 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.