blob: 5a8ae42891acd57990b5f891f91c070f504333dc [file]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/http/http_cache_writers.h"
#include <algorithm>
#include <optional>
#include <utility>
#include "base/byte_size.h"
#include "base/containers/span.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/numerics/safe_conversions.h"
#include "base/pickle.h"
#include "base/task/single_thread_task_runner.h"
#include "net/base/features.h"
#include "net/base/net_errors.h"
#include "net/disk_cache/disk_cache.h"
#include "net/http/cache_body_compressor.h"
#include "net/http/http_cache_transaction.h"
#include "net/http/http_response_info.h"
#include "net/http/http_status_code.h"
#include "net/http/partial_data.h"
#include "net/shared_dictionary/shared_dictionary_constants.h"
namespace net {
namespace {
constexpr int kMaxFinalizeRounds = 32;
bool IsValidResponseForWriter(bool is_partial,
const HttpResponseInfo* response_info) {
if (!response_info->headers.get()) {
return false;
}
// Return false if the response code sent by the server is garbled.
// Both 200 and 304 are valid since concurrent writing is supported.
if (!is_partial &&
(response_info->headers->response_code() != HTTP_OK &&
response_info->headers->response_code() != HTTP_NOT_MODIFIED)) {
return false;
}
return true;
}
} // namespace
HttpCache::Writers::TransactionInfo::TransactionInfo(PartialData* partial_data,
const bool is_truncated,
HttpResponseInfo info)
: partial(partial_data), truncated(is_truncated), response_info(info) {}
HttpCache::Writers::TransactionInfo::~TransactionInfo() = default;
HttpCache::Writers::TransactionInfo::TransactionInfo(const TransactionInfo&) =
default;
HttpCache::Writers::Writers(HttpCache* cache,
scoped_refptr<HttpCache::ActiveEntry> entry)
: cache_(cache), entry_(entry) {
DCHECK(cache_);
DCHECK(entry_);
}
HttpCache::Writers::~Writers() = default;
int HttpCache::Writers::Read(scoped_refptr<IOBuffer> buf,
int buf_len,
CompletionOnceCallback callback,
Transaction* transaction) {
DCHECK(buf);
DCHECK_GT(buf_len, 0);
DCHECK(!callback.is_null());
DCHECK(transaction);
// If another transaction invoked a Read which is currently ongoing, then
// this transaction waits for the read to complete and gets its buffer filled
// with the data returned from that read.
if (next_state_ != State::NONE) {
WaitingForRead read_info(buf, buf_len, std::move(callback));
waiting_for_read_.emplace(transaction, std::move(read_info));
return ERR_IO_PENDING;
}
DCHECK_EQ(next_state_, State::NONE);
DCHECK(callback_.is_null());
DCHECK_EQ(nullptr, active_transaction_);
DCHECK(HasTransaction(transaction));
active_transaction_ = transaction;
read_buf_ = std::move(buf);
io_buf_len_ = buf_len;
next_state_ = State::NETWORK_READ;
int rv = DoLoop(OK);
if (rv == ERR_IO_PENDING) {
callback_ = std::move(callback);
}
return rv;
}
bool HttpCache::Writers::StopCaching(bool keep_entry) {
// If this is the only transaction in Writers, then stopping will be
// successful. If not, then we will not stop caching since there are
// other consumers waiting to read from the cache.
if (all_writers_.size() != 1) {
return false;
}
network_read_only_ = true;
// If compressed bytes have been written, the entry is corrupt (mixed
// uncompressed + compressed). Force doom regardless of keep_entry.
if (compressing_for_cache_ && compressor_ &&
compressor_->total_input_bytes() > 0) {
keep_entry = false;
}
if (!keep_entry) {
should_keep_entry_ = false;
cache_->WritersDoomEntryRestartTransactions(entry_.get());
}
compressor_.reset();
compressing_for_cache_ = false;
return true;
}
void HttpCache::Writers::AddTransaction(
Transaction* transaction,
ParallelWritingPattern initial_writing_pattern,
RequestPriority priority,
const TransactionInfo& info) {
DCHECK(transaction);
ParallelWritingPattern writers_pattern;
DCHECK(CanAddWriters(&writers_pattern));
DCHECK_EQ(0u, all_writers_.count(transaction));
// Set truncation related information.
response_info_truncation_ = info.response_info;
should_keep_entry_ =
IsValidResponseForWriter(info.partial != nullptr, &(info.response_info));
if (all_writers_.empty()) {
DCHECK_EQ(PARALLEL_WRITING_NONE, parallel_writing_pattern_);
parallel_writing_pattern_ = initial_writing_pattern;
if (parallel_writing_pattern_ != PARALLEL_WRITING_JOIN) {
is_exclusive_ = true;
}
} else {
DCHECK_EQ(PARALLEL_WRITING_JOIN, parallel_writing_pattern_);
}
if (info.partial && !info.truncated) {
DCHECK(!partial_do_not_truncate_);
partial_do_not_truncate_ = true;
}
std::pair<Transaction*, TransactionInfo> writer(transaction, info);
all_writers_.insert(writer);
priority_ = std::max(priority, priority_);
if (network_transaction_) {
network_transaction_->SetPriority(priority_);
}
}
void HttpCache::Writers::SetNetworkTransaction(
Transaction* transaction,
std::unique_ptr<HttpTransaction> network_transaction) {
DCHECK_EQ(1u, all_writers_.count(transaction));
DCHECK(network_transaction);
DCHECK(!network_transaction_);
network_transaction_ = std::move(network_transaction);
network_transaction_->SetPriority(priority_);
}
void HttpCache::Writers::ResetNetworkTransaction() {
DCHECK(is_exclusive_);
DCHECK_EQ(1u, all_writers_.size());
DCHECK(all_writers_.begin()->second.partial);
network_transaction_.reset();
}
void HttpCache::Writers::RemoveTransaction(Transaction* transaction,
bool success) {
EraseTransaction(transaction, OK);
if (!all_writers_.empty()) {
return;
}
if (!success && ShouldTruncate()) {
TruncateEntry();
}
// Destroys `this`.
cache_->WritersDoneWritingToEntry(entry_, success, should_keep_entry_,
TransactionSet());
}
void HttpCache::Writers::EraseTransaction(Transaction* transaction,
int result) {
// The transaction should be part of all_writers.
auto it = all_writers_.find(transaction);
CHECK(it != all_writers_.end());
EraseTransaction(it, result);
}
HttpCache::Writers::TransactionMap::iterator
HttpCache::Writers::EraseTransaction(TransactionMap::iterator it, int result) {
Transaction* transaction = it->first;
transaction->WriterAboutToBeRemovedFromEntry(result);
auto return_it = all_writers_.erase(it);
if (all_writers_.empty() && next_state_ == State::NONE) {
// This needs to be called to handle the edge case where even before Read is
// invoked all transactions are removed. In that case the
// network_transaction_ will still have a valid request info and so it
// should be destroyed before its consumer is destroyed (request info
// is a raw pointer owned by its consumer).
network_transaction_.reset();
} else {
UpdatePriority();
}
if (active_transaction_ == transaction) {
active_transaction_ = nullptr;
} else {
// If waiting for read, remove it from the map.
waiting_for_read_.erase(transaction);
}
return return_it;
}
void HttpCache::Writers::UpdatePriority() {
// Get the current highest priority.
RequestPriority current_highest = MINIMUM_PRIORITY;
for (auto& writer : all_writers_) {
Transaction* transaction = writer.first;
current_highest = std::max(transaction->priority(), current_highest);
}
if (priority_ != current_highest) {
if (network_transaction_) {
network_transaction_->SetPriority(current_highest);
}
priority_ = current_highest;
}
}
void HttpCache::Writers::CloseConnectionOnDestruction() {
if (network_transaction_) {
network_transaction_->CloseConnectionOnDestruction();
}
}
bool HttpCache::Writers::ContainsOnlyIdleWriters() const {
return waiting_for_read_.empty() && !active_transaction_;
}
bool HttpCache::Writers::CanAddWriters(ParallelWritingPattern* reason) {
*reason = parallel_writing_pattern_;
if (all_writers_.empty()) {
return true;
}
return !is_exclusive_ && !network_read_only_;
}
void HttpCache::Writers::ProcessFailure(int error) {
// Notify waiting_for_read_ of the failure. Tasks will be posted for all the
// transactions.
CompleteWaitingForReadTransactions(error);
// Idle readers should fail when Read is invoked on them.
RemoveIdleWriters(error);
}
void HttpCache::Writers::TruncateEntry() {
DCHECK(ShouldTruncate());
auto data = base::MakeRefCounted<PickledIOBuffer>(
response_info_truncation_.MakePickle(
/*skip_transient_headers=*/true,
/*response_truncated=*/true));
io_buf_len_ = data->size();
entry_->GetEntry()->WriteData(kResponseInfoIndex, 0, data.get(), io_buf_len_,
base::DoNothing(), true);
}
void HttpCache::Writers::UpdateEncodedBodySizeInCacheEntry() {
if (!network_transaction_ || !entry_) {
return;
}
// Only store the encoded body size for shared dictionary responses, where
// the cache stores the decompressed body and we need to remember the
// original on-the-wire size. For other content encodings (e.g., gzip),
// decompression happens after cache, so the cached size already matches
// the encoded size.
if (!response_info_truncation_.did_use_shared_dictionary) {
return;
}
base::ByteSize encoded_body_bytes =
network_transaction_->GetReceivedBodyBytes();
if (encoded_body_bytes.is_zero()) {
return;
}
response_info_truncation_.encoded_body_size = encoded_body_bytes;
auto data = base::MakeRefCounted<PickledIOBuffer>(
response_info_truncation_.MakePickle(
/*skip_transient_headers=*/true,
/*response_truncated=*/false));
io_buf_len_ = data->size();
entry_->GetEntry()->WriteData(kResponseInfoIndex, 0, data.get(), io_buf_len_,
base::DoNothing(), true);
}
bool HttpCache::Writers::ShouldTruncate() {
// A partial zstd frame is undecodable. Doom rather than truncate.
if (compressing_for_cache_) {
should_keep_entry_ = false;
return false;
}
// Don't set the flag for sparse entries or for entries that cannot be
// resumed.
if (!should_keep_entry_ || partial_do_not_truncate_) {
return false;
}
// Check the response headers for strong validators.
// Note that if this is a 206, content-length was already fixed after calling
// PartialData::ResponseHeadersOK().
std::optional<base::ByteSize> content_length =
response_info_truncation_.headers->GetContentLength();
if (!content_length.has_value() || content_length->is_zero() ||
response_info_truncation_.headers->HasHeaderValue("Accept-Ranges",
"none") ||
!response_info_truncation_.headers->HasStrongValidators()) {
should_keep_entry_ = false;
return false;
}
// Double check that there is something worth keeping.
int current_size = entry_->GetEntry()->GetDataSize(kResponseContentIndex);
if (!current_size) {
should_keep_entry_ = false;
return false;
}
if (response_info_truncation_.headers->HasHeader("Content-Encoding")) {
should_keep_entry_ = false;
return false;
}
if (current_size < 0 ||
content_length->InBytes() <= base::as_unsigned(current_size)) {
return false;
}
return true;
}
LoadState HttpCache::Writers::GetLoadState() const {
if (network_transaction_) {
return network_transaction_->GetLoadState();
}
return LOAD_STATE_IDLE;
}
HttpCache::Writers::WaitingForRead::WaitingForRead(
scoped_refptr<IOBuffer> buf,
int len,
CompletionOnceCallback consumer_callback)
: read_buf(std::move(buf)),
read_buf_len(len),
callback(std::move(consumer_callback)) {
DCHECK(read_buf);
DCHECK_GT(len, 0);
DCHECK(!callback.is_null());
}
HttpCache::Writers::WaitingForRead::~WaitingForRead() = default;
HttpCache::Writers::WaitingForRead::WaitingForRead(WaitingForRead&&) = default;
int HttpCache::Writers::DoLoop(int result) {
DCHECK_NE(State::UNSET, next_state_);
DCHECK_NE(State::NONE, next_state_);
int rv = result;
do {
State state = next_state_;
next_state_ = State::UNSET;
switch (state) {
case State::NETWORK_READ:
DCHECK_EQ(OK, rv);
rv = DoNetworkRead();
break;
case State::NETWORK_READ_COMPLETE:
rv = DoNetworkReadComplete(rv);
break;
case State::CACHE_WRITE_DATA:
rv = DoCacheWriteData(rv);
break;
case State::CACHE_WRITE_DATA_COMPLETE:
rv = DoCacheWriteDataComplete(rv);
break;
case State::CACHE_WRITE_COMPRESSED_FINALIZE:
rv = DoCacheWriteCompressedFinalize(rv);
break;
case State::CACHE_WRITE_COMPRESSED_FINALIZE_COMPLETE:
rv = DoCacheWriteCompressedFinalizeComplete(rv);
break;
case State::CACHE_WRITE_COMPRESSED_METADATA:
rv = DoCacheWriteCompressedMetadata();
break;
case State::CACHE_WRITE_COMPRESSED_METADATA_COMPLETE:
rv = DoCacheWriteCompressedMetadataComplete(rv);
break;
case State::UNSET:
NOTREACHED() << "bad state";
case State::NONE:
// Do Nothing.
break;
}
} while (next_state_ != State::NONE && rv != ERR_IO_PENDING);
if (next_state_ != State::NONE) {
if (rv != ERR_IO_PENDING && !callback_.is_null()) {
std::move(callback_).Run(rv);
}
return rv;
}
// Save the callback as |this| may be destroyed when |cache_callback_| is run.
// Note that |callback_| is intentionally reset even if it is not run.
CompletionOnceCallback callback = std::move(callback_);
read_buf_ = nullptr;
DCHECK(!all_writers_.empty() || cache_callback_);
if (cache_callback_) {
std::move(cache_callback_).Run();
}
// |this| may have been destroyed in the |cache_callback_|.
if (rv != ERR_IO_PENDING && !callback.is_null()) {
std::move(callback).Run(rv);
}
return rv;
}
int HttpCache::Writers::DoNetworkRead() {
DCHECK(network_transaction_);
next_state_ = State::NETWORK_READ_COMPLETE;
// TODO(crbug.com/40089413): This is a partial mitigation. When
// reading from the network, a valid HttpNetworkTransaction must be always
// available.
if (!network_transaction_) {
return ERR_FAILED;
}
CompletionOnceCallback io_callback = base::BindOnce(
&HttpCache::Writers::OnIOComplete, weak_factory_.GetWeakPtr());
return network_transaction_->Read(read_buf_.get(), io_buf_len_,
std::move(io_callback));
}
int HttpCache::Writers::DoNetworkReadComplete(int result) {
if (result < 0) {
next_state_ = State::NONE;
OnNetworkReadFailure(result);
return result;
}
next_state_ = State::CACHE_WRITE_DATA;
return result;
}
void HttpCache::Writers::OnNetworkReadFailure(int result) {
ProcessFailure(result);
if (active_transaction_) {
EraseTransaction(active_transaction_, result);
}
active_transaction_ = nullptr;
if (ShouldTruncate()) {
TruncateEntry();
}
SetCacheCallback(false, TransactionSet());
}
int HttpCache::Writers::DoCacheWriteData(int num_bytes) {
next_state_ = State::CACHE_WRITE_DATA_COMPLETE;
write_len_ = num_bytes;
if (!num_bytes || network_read_only_) {
if (compressing_for_cache_) {
compressed_write_len_ = 0;
}
return num_bytes;
}
int current_size = entry_->GetEntry()->GetDataSize(kResponseContentIndex);
CompletionOnceCallback io_callback = base::BindOnce(
&HttpCache::Writers::OnIOComplete, weak_factory_.GetWeakPtr());
int rv = 0;
PartialData* partial = nullptr;
// The active transaction must be alive if this is a partial request, as
// partial requests are exclusive and hence will always be the active
// transaction.
// TODO(shivanisha): When partial requests support parallel writing, this
// assumption will not be true.
if (active_transaction_) {
partial = all_writers_.find(active_transaction_)->second.partial;
}
if (!partial) {
last_disk_cache_access_start_time_ = base::TimeTicks::Now();
// On the first data chunk, decide whether to compress.
// Require single writer to prevent parallel writer corruption.
if (current_size == 0 && !compressing_for_cache_ && !compressor_ &&
all_writers_.size() == 1 && ShouldCompressForCache()) {
InitCompression();
}
if (compressing_for_cache_) {
rv = CompressAndWriteBlock(num_bytes, current_size,
std::move(io_callback));
} else {
rv = entry_->GetEntry()->WriteData(kResponseContentIndex, current_size,
read_buf_.get(), num_bytes,
std::move(io_callback), true);
}
} else {
rv = partial->CacheWrite(entry_->GetEntry(), read_buf_.get(), num_bytes,
std::move(io_callback));
}
return rv;
}
int HttpCache::Writers::DoCacheWriteDataComplete(int result) {
DCHECK(!all_writers_.empty());
DCHECK_GE(write_len_, 0);
int expected_result =
compressing_for_cache_ ? compressed_write_len_ : write_len_;
if (result != expected_result) {
next_state_ = State::NONE;
// Note that it is possible for cache write to fail if the size of the file
// exceeds the per-file limit.
OnCacheWriteFailure();
// |active_transaction_| can continue reading from the network.
return write_len_;
}
if (!last_disk_cache_access_start_time_.is_null() && active_transaction_ &&
!all_writers_.find(active_transaction_)->second.partial) {
active_transaction_->AddDiskCacheWriteTime(
base::TimeTicks::Now() - last_disk_cache_access_start_time_);
last_disk_cache_access_start_time_ = base::TimeTicks();
}
next_state_ = State::NONE;
OnDataReceived(write_len_);
// If OnDataReceived started the compressed-finalize state machine,
// let DoLoop continue processing those states before returning to the
// caller. The finalize path will return write_len_ when complete.
if (next_state_ != State::NONE) {
return OK;
}
return write_len_;
}
void HttpCache::Writers::OnDataReceived(int result) {
DCHECK(!all_writers_.empty());
auto it = all_writers_.find(active_transaction_);
bool is_partial =
active_transaction_ != nullptr && it->second.partial != nullptr;
// Partial transaction will process the result, return from here.
// This is done because partial requests handling require an awareness of both
// headers and body state machines as they might have to go to the headers
// phase for the next range, so it cannot be completely handled here.
if (is_partial) {
active_transaction_ = nullptr;
return;
}
if (result == 0) {
// Check if the response is actually completed or if not, attempt to mark
// the entry as truncated in OnNetworkReadFailure.
// For CDT, Content-Length is wire bytes while the cache stores decompressed
// plaintext. Compare against actual wire bytes received to detect
// truncation.
DCHECK(network_transaction_);
int64_t bytes_received =
compressing_for_cache_
? network_transaction_->GetReceivedBodyBytes().InBytes()
: entry_->GetEntry()->GetDataSize(kResponseContentIndex);
const HttpResponseInfo* response_info =
network_transaction_->GetResponseInfo();
std::optional<base::ByteSize> content_length =
response_info->headers->GetContentLength();
if (content_length &&
static_cast<int64_t>(content_length->InBytes()) > bytes_received) {
OnNetworkReadFailure(result);
return;
}
// If compressing, finalize the zstd frame and persist metadata before
// completing. The normal EOF cleanup runs inside
// DoCacheWriteCompressedFinalizeComplete after the frame is flushed.
if (compressing_for_cache_) {
next_state_ = State::CACHE_WRITE_COMPRESSED_FINALIZE;
return;
}
// Store the original encoded body size in the cached response info so
// that future reads from cache can report the correct encodedBodySize
// for Resource Timing.
UpdateEncodedBodySizeInCacheEntry();
CompleteWritingAndNotifyTransactions();
return;
}
// Notify waiting_for_read_. Tasks will be posted for all the
// transactions.
CompleteWaitingForReadTransactions(write_len_);
active_transaction_ = nullptr;
}
void HttpCache::Writers::OnCacheWriteFailure() {
DLOG(ERROR) << "failed to write response data to cache";
ProcessFailure(ERR_CACHE_WRITE_FAILURE);
// Now writers will only be reading from the network.
network_read_only_ = true;
compressor_.reset();
compressing_for_cache_ = false;
active_transaction_ = nullptr;
should_keep_entry_ = false;
if (all_writers_.empty()) {
SetCacheCallback(false, TransactionSet());
} else {
cache_->WritersDoomEntryRestartTransactions(entry_.get());
}
}
void HttpCache::Writers::CompleteWaitingForReadTransactions(int result) {
for (auto it = waiting_for_read_.begin(); it != waiting_for_read_.end();) {
Transaction* transaction = it->first;
int callback_result = result;
if (result >= 0) { // success
// Save the data in the waiting transaction's read buffer.
it->second.write_len = std::min(it->second.read_buf_len, result);
it->second.read_buf->span().copy_prefix_from(
read_buf_->first(it->second.write_len));
callback_result = it->second.write_len;
}
// Post task to notify transaction.
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(it->second.callback), callback_result));
it = waiting_for_read_.erase(it);
// If its response completion or failure, this transaction needs to be
// removed from writers.
if (result <= 0) {
EraseTransaction(transaction, result);
}
}
}
void HttpCache::Writers::RemoveIdleWriters(int result) {
// Since this is only for idle transactions, waiting_for_read_
// should be empty.
DCHECK(waiting_for_read_.empty());
for (auto it = all_writers_.begin(); it != all_writers_.end();) {
Transaction* transaction = it->first;
if (transaction == active_transaction_) {
it++;
continue;
}
it = EraseTransaction(it, result);
}
}
void HttpCache::Writers::SetCacheCallback(bool success,
const TransactionSet& make_readers) {
DCHECK(!cache_callback_);
cache_callback_ = base::BindOnce(&HttpCache::WritersDoneWritingToEntry,
cache_->GetWeakPtr(), entry_, success,
should_keep_entry_, make_readers);
}
bool HttpCache::Writers::ShouldCompressForCache() const {
#if defined(NET_DISABLE_ZSTD_COMPRESS)
return false;
#else
if (!base::FeatureList::IsEnabled(features::kHttpCacheZstdCompression)) {
return false;
}
if (!base::FeatureList::IsEnabled(features::kHttpCacheZstdDecompression)) {
return false;
}
if (!response_info_truncation_.did_use_shared_dictionary) {
return false;
}
// CDT responses carry Content-Encoding: dcb or dcz, which is fine -
// the shared dictionary layer already decoded them. Reject only standard
// transport encodings (gzip, br, etc.) to avoid double-compression.
std::optional<std::string> content_encoding =
response_info_truncation_.headers->GetNormalizedHeader(
"Content-Encoding");
if (content_encoding &&
*content_encoding !=
shared_dictionary::kSharedBrotliContentEncodingName &&
*content_encoding != shared_dictionary::kSharedZstdContentEncodingName) {
return false;
}
return true;
#endif
}
void HttpCache::Writers::InitCompression() {
compressor_ = std::make_unique<CacheBodyCompressor>();
if (!compressor_->Init()) {
compressor_.reset();
return;
}
if (const auto max_size = cache_->compression_max_size_for_testing()) {
compressor_->set_max_uncompressed_size_for_testing(*max_size); // IN-TEST
}
compressing_for_cache_ = true;
}
bool HttpCache::Writers::CanJoin() const {
// compressor_ is transiently null during the metadata write after
// finalization, so block joins once compression has started.
if (compressing_for_cache_) {
return compressor_ && compressor_->total_input_bytes() == 0;
}
return true;
}
int HttpCache::Writers::CompressAndWriteBlock(
int num_bytes,
int current_size,
CompletionOnceCallback io_callback) {
CHECK(compressor_);
const int compressed = compressor_->Compress(
read_buf_->span().first(static_cast<size_t>(num_bytes)),
/*is_last_chunk=*/false);
if (compressed < 0) {
return ERR_CACHE_WRITE_FAILURE;
}
if (compressed == 0) {
compressed_write_len_ = 0;
return 0;
}
compressed_write_len_ = compressed;
return entry_->GetEntry()->WriteData(
kResponseContentIndex, current_size, compressor_->output_buffer(),
compressed_write_len_, std::move(io_callback), true);
}
int HttpCache::Writers::DoCacheWriteCompressedFinalize(int result) {
DCHECK_EQ(OK, result);
// StopCaching() may have been called during a prior async disk write,
// resetting compressor_. The entry is already doomed; clean up.
if (!compressor_) {
next_state_ = State::NONE;
should_keep_entry_ = false;
CompleteWaitingForReadTransactions(write_len_);
if (active_transaction_) {
EraseTransaction(active_transaction_, 0);
}
active_transaction_ = nullptr;
SetCacheCallback(false, TransactionSet());
return write_len_;
}
next_state_ = State::CACHE_WRITE_COMPRESSED_FINALIZE_COMPLETE;
const int remaining = compressor_->Finalize();
if (remaining < 0) {
return ERR_CACHE_WRITE_FAILURE;
}
finalize_remaining_ = remaining;
compressed_write_len_ = base::checked_cast<int>(compressor_->output_length());
if (compressed_write_len_ == 0 && finalize_remaining_ == 0) {
return OK;
}
int current_size = entry_->GetEntry()->GetDataSize(kResponseContentIndex);
CompletionOnceCallback io_callback = base::BindOnce(
&HttpCache::Writers::OnIOComplete, weak_factory_.GetWeakPtr());
return entry_->GetEntry()->WriteData(
kResponseContentIndex, current_size, compressor_->output_buffer(),
compressed_write_len_, std::move(io_callback), true);
}
int HttpCache::Writers::DoCacheWriteCompressedFinalizeComplete(int result) {
DCHECK(!all_writers_.empty());
if (result != compressed_write_len_) {
// Finalize write failed, but the download itself completed successfully.
// Doom the entry but return success to the consumer.
next_state_ = State::NONE;
compressor_.reset();
should_keep_entry_ = false;
CompleteWaitingForReadTransactions(write_len_);
if (active_transaction_) {
EraseTransaction(active_transaction_, 0);
}
active_transaction_ = nullptr;
SetCacheCallback(false, TransactionSet());
return write_len_;
}
// StopCaching() may have been called during the async disk write,
// resetting compressor_. The entry is already doomed; clean up.
if (!compressor_) {
next_state_ = State::NONE;
should_keep_entry_ = false;
CompleteWaitingForReadTransactions(write_len_);
if (active_transaction_) {
EraseTransaction(active_transaction_, 0);
}
active_transaction_ = nullptr;
SetCacheCallback(false, TransactionSet());
return write_len_;
}
// If zstd still has more output to flush, loop back.
if (finalize_remaining_ > 0) {
if (++finalize_rounds_ > kMaxFinalizeRounds) {
DVLOG(1) << "zstd finalization exceeded max rounds";
next_state_ = State::NONE;
compressor_.reset();
should_keep_entry_ = false;
CompleteWaitingForReadTransactions(write_len_);
if (active_transaction_) {
EraseTransaction(active_transaction_, 0);
}
active_transaction_ = nullptr;
SetCacheCallback(false, TransactionSet());
return write_len_;
}
next_state_ = State::CACHE_WRITE_COMPRESSED_FINALIZE;
return OK;
}
// Frame complete. Persist the uncompressed size and encoded body size
// metadata in a single awaited disk write.
UpdateResponseInfoForCompression();
compressor_.reset();
next_state_ = State::CACHE_WRITE_COMPRESSED_METADATA;
return OK;
}
void HttpCache::Writers::UpdateResponseInfoForCompression() {
CHECK(compressor_);
response_info_truncation_.zstd_uncompressed_body_size =
compressor_->total_input_bytes();
// Also set encoded_body_size so we don't need a second metadata write
// (UpdateEncodedBodySizeInCacheEntry would write the same index entry again).
if (network_transaction_) {
base::ByteSize encoded_body_bytes =
network_transaction_->GetReceivedBodyBytes();
if (!encoded_body_bytes.is_zero()) {
response_info_truncation_.encoded_body_size = encoded_body_bytes;
}
}
metadata_buf_ = base::MakeRefCounted<PickledIOBuffer>(
response_info_truncation_.MakePickle(
/*skip_transient_headers=*/true,
/*response_truncated=*/false));
metadata_buf_len_ = metadata_buf_->size();
}
int HttpCache::Writers::DoCacheWriteCompressedMetadata() {
DCHECK(metadata_buf_);
next_state_ = State::CACHE_WRITE_COMPRESSED_METADATA_COMPLETE;
CompletionOnceCallback io_callback = base::BindOnce(
&HttpCache::Writers::OnIOComplete, weak_factory_.GetWeakPtr());
return entry_->GetEntry()->WriteData(kResponseInfoIndex, 0,
metadata_buf_.get(), metadata_buf_len_,
std::move(io_callback), true);
}
int HttpCache::Writers::DoCacheWriteCompressedMetadataComplete(int result) {
DCHECK(!all_writers_.empty());
if (result != metadata_buf_len_) {
// Metadata write failed. The body on disk is compressed but has no
// decompression marker - doom the entry to prevent serving raw zstd.
next_state_ = State::NONE;
metadata_buf_.reset();
should_keep_entry_ = false;
CompleteWaitingForReadTransactions(write_len_);
if (active_transaction_) {
EraseTransaction(active_transaction_, 0);
}
active_transaction_ = nullptr;
SetCacheCallback(false, TransactionSet());
return write_len_;
}
metadata_buf_.reset();
next_state_ = State::NONE;
CompleteWritingAndNotifyTransactions();
return write_len_;
}
void HttpCache::Writers::CompleteWritingAndNotifyTransactions() {
DCHECK_EQ(next_state_, State::NONE);
if (active_transaction_) {
EraseTransaction(active_transaction_, 0);
}
active_transaction_ = nullptr;
CompleteWaitingForReadTransactions(write_len_);
CHECK(ContainsOnlyIdleWriters());
if (compressing_for_cache_) {
// Idle writers haven't read the updated metadata
// (zstd_uncompressed_body_size) so they cannot become direct readers.
// Erase them with ERR_CACHE_RACE to trigger a full restart.
for (auto it = all_writers_.begin(); it != all_writers_.end();) {
it = EraseTransaction(it, ERR_CACHE_RACE);
}
// success=false with should_keep_entry_=true causes
// WritersDoneWritingToEntry to call RestartHeadersPhaseTransactions,
// which restarts done_headers_queue_ transactions via ERR_CACHE_RACE.
SetCacheCallback(false, TransactionSet());
return;
}
TransactionSet make_readers;
for (auto& writer : all_writers_) {
make_readers.insert(writer.first);
}
all_writers_.clear();
SetCacheCallback(true, make_readers);
}
void HttpCache::Writers::OnIOComplete(int result) {
DoLoop(result);
}
} // namespace net