blob: 9e5c8d2681cb821ae7432903581b7fad22d41605 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/services/storage/indexed_db/scopes/leveldb_scope.h"
#include <memory>
#include <sstream>
#include "base/compiler_specific.h"
#include "base/debug/stack_trace.h"
#include "base/memory/ptr_util.h"
#include "base/optional.h"
#include "components/services/storage/indexed_db/scopes/leveldb_scopes_coding.h"
#include "third_party/leveldatabase/src/include/leveldb/comparator.h"
#include "third_party/leveldatabase/src/include/leveldb/db.h"
namespace content {
namespace {
#if DCHECK_IS_ON()
leveldb::Slice Uint8VectorToSlice(const std::vector<uint8_t>& vector) {
return leveldb::Slice(reinterpret_cast<const char*>(vector.data()),
vector.size());
}
#endif
// Tests if the given key is before the end of a range.
bool IsKeyBeforeEndOfRange(const leveldb::Comparator* comparator,
const leveldb::Slice& key,
const leveldb::Slice& end,
bool end_exclusive) {
return (end_exclusive ? comparator->Compare(key, end) < 0
: comparator->Compare(key, end) <= 0);
}
} // namespace
// Adds undo log tasks to the given LevelDBScope for every entry found in the
// WriteBatch that this is iterating.
// Taken partially, the resulting undo log is technically incorrect. Two
// operations for the same key, for example Put(K, V1) and Put(K, V2), will
// result in an undo log containing either Put(K, old_value_for_k) twice or
// Delete(K) twice. This is OK, because recovery always applies the entire undo
// log, so it only matters that each key's final operation is correct.
class LevelDBScope::UndoLogWriter : public leveldb::WriteBatch::Handler {
public:
UndoLogWriter(LevelDBScope* scope, leveldb::DB* db)
: scope_(scope), db_(db) {}
~UndoLogWriter() override = default;
void Put(const leveldb::Slice& key, const leveldb::Slice& value) override {
DCHECK(scope_->IsUndoLogMode());
if (UNLIKELY(!error_.ok()))
return;
if (scope_->CanSkipWritingUndoEntry(key))
return;
leveldb::ReadOptions read_options;
read_options.verify_checksums = true;
// Since the values being read here are going to be overwritten as soon as
// the write batch is written, the block will basically be obsolete. Thus,
// don't bother caching.
read_options.fill_cache = false;
read_buffer_.clear();
leveldb::Status s = db_->Get(read_options, key, &read_buffer_);
if (s.IsNotFound()) {
scope_->AddUndoDeleteTask(key.ToString());
return;
}
if (UNLIKELY(!s.ok())) {
error_ = std::move(s);
return;
}
scope_->AddUndoPutTask(key.ToString(), std::move(read_buffer_));
}
void Delete(const leveldb::Slice& key) override {
DCHECK(scope_->IsUndoLogMode());
if (UNLIKELY(!error_.ok()))
return;
if (scope_->CanSkipWritingUndoEntry(key))
return;
leveldb::ReadOptions read_options;
read_options.verify_checksums = true;
// Since the values being read here are going to be overwritten as soon as
// the write batch is written, the block will basically be obsolete. Thus,
// don't bother caching.
read_options.fill_cache = false;
read_buffer_.clear();
leveldb::Status s = db_->Get(read_options, key, &read_buffer_);
if (s.IsNotFound())
return;
if (UNLIKELY(!s.ok())) {
error_ = std::move(s);
return;
}
scope_->AddUndoPutTask(key.ToString(), std::move(read_buffer_));
}
const leveldb::Status& error() const { return error_; }
private:
LevelDBScope* const scope_;
leveldb::DB* const db_;
std::string read_buffer_;
leveldb::Status error_ = leveldb::Status::OK();
};
LevelDBScope::EmptyRangeLessThan::EmptyRangeLessThan() = default;
LevelDBScope::EmptyRangeLessThan::EmptyRangeLessThan(
const leveldb::Comparator* comparator)
: comparator_(comparator) {}
LevelDBScope::EmptyRangeLessThan& LevelDBScope::EmptyRangeLessThan::operator=(
const LevelDBScope::EmptyRangeLessThan& other) = default;
// The ranges are expected to be disjoint.
bool LevelDBScope::EmptyRangeLessThan::operator()(const EmptyRange& lhs,
const EmptyRange& rhs) const {
return comparator_->Compare(lhs.first, rhs.first) < 0;
}
LevelDBScope::LevelDBScope(
int64_t scope_id,
std::vector<uint8_t> prefix,
size_t write_batch_size,
scoped_refptr<LevelDBState> level_db,
std::vector<ScopeLock> locks,
std::vector<std::pair<std::string, std::string>> empty_ranges,
RollbackCallback rollback_callback,
TearDownCallback tear_down_callback)
: scope_id_(scope_id),
prefix_(std::move(prefix)),
write_batch_size_(write_batch_size),
level_db_(std::move(level_db)),
locks_(std::move(locks)),
rollback_callback_(std::move(rollback_callback)),
tear_down_callback_(std::move(tear_down_callback)) {
DCHECK(!locks_.empty());
std::vector<std::pair<EmptyRange, bool>> map_values;
map_values.reserve(empty_ranges.size());
for (EmptyRange& range : empty_ranges) {
// Moving the range here technically messes up the sorting order of
// empty_ranges, but it's not used again anyways so we don't mind.
map_values.emplace_back(std::move(range), false);
}
empty_ranges_ = base::flat_map<EmptyRange, bool, EmptyRangeLessThan>(
std::move(map_values), EmptyRangeLessThan(level_db_->comparator()));
#if DCHECK_IS_ON()
ValidateEmptyRanges();
#endif
}
LevelDBScope::~LevelDBScope() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (UNLIKELY(has_written_to_disk_ && !committed_ && rollback_callback_)) {
DCHECK(undo_sequence_number_ < std::numeric_limits<int64_t>::max() ||
cleanup_sequence_number_ > 0)
<< "A reverting scope that has written to disk must have either an "
"undo or cleanup task written to it. undo_sequence_number_: "
<< undo_sequence_number_
<< ", cleanup_sequence_number_: " << cleanup_sequence_number_;
leveldb::Status status =
std::move(rollback_callback_).Run(scope_id_, std::move(locks_));
if (!status.ok())
tear_down_callback_.Run(status);
}
}
leveldb::Status LevelDBScope::Put(const leveldb::Slice& key,
const leveldb::Slice& value) {
// This has to be used for IsInDeferredDeletionRange, so it might as well
// surround all the DCHECKs.
#if DCHECK_IS_ON()
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!key.starts_with(Uint8VectorToSlice(prefix_)));
DCHECK(!committed_);
DCHECK(!locks_.empty());
DCHECK(!IsInDeferredDeletionRange(key))
<< "Cannot put a value in a range that will be deleted later.";
#endif
buffer_batch_.Put(key, value);
buffer_batch_empty_ = false;
if (GetMemoryUsage() > write_batch_size_)
return WriteChangesAndUndoLogInternal(false);
return leveldb::Status::OK();
}
leveldb::Status LevelDBScope::Delete(const leveldb::Slice& key) {
// This has to be used for IsInDeferredDeletionRange, so it might as well
// surround all the DCHECKs.
#if DCHECK_IS_ON()
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!key.starts_with(Uint8VectorToSlice(prefix_)));
DCHECK(!committed_);
DCHECK(!locks_.empty());
DCHECK(!IsInDeferredDeletionRange(key))
<< "Cannot delete value in a range that will be deleted later.";
#endif
buffer_batch_.Delete(key);
buffer_batch_empty_ = false;
if (GetMemoryUsage() > write_batch_size_)
return WriteChangesAndUndoLogInternal(false);
return leveldb::Status::OK();
}
leveldb::Status LevelDBScope::DeleteRange(const leveldb::Slice& begin,
const leveldb::Slice& end,
LevelDBScopeDeletionMode mode) {
#if DCHECK_IS_ON()
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!begin.starts_with(Uint8VectorToSlice(prefix_)));
DCHECK(!end.starts_with(Uint8VectorToSlice(prefix_)));
DCHECK(!committed_);
DCHECK(!locks_.empty());
switch (mode) {
case LevelDBScopeDeletionMode::kDeferred:
case LevelDBScopeDeletionMode::kDeferredWithCompaction:
deferred_delete_ranges_.emplace_back(begin.ToString(), end.ToString());
break;
default:
break;
}
#endif
bool end_exclusive = true;
switch (mode) {
case LevelDBScopeDeletionMode::kDeferred:
SetModeToUndoLog();
AddCleanupDeleteRangeTask(begin.ToString(), end.ToString());
return leveldb::Status::OK();
case LevelDBScopeDeletionMode::kDeferredWithCompaction:
SetModeToUndoLog();
AddCleanupDeleteAndCompactRangeTask(begin.ToString(), end.ToString());
return leveldb::Status::OK();
case LevelDBScopeDeletionMode::kImmediateWithRangeEndExclusive:
break;
case LevelDBScopeDeletionMode::kImmediateWithRangeEndInclusive:
end_exclusive = false;
break;
}
// This method uses its own algorithm to generate the undo log tasks because
// it can take advantage of the current |value()| already loaded in the
// iterator. So process any existing changes (and generate any needed undo log
// tasks) to start with an empty |buffer_batch_|.
leveldb::Status s = WriteChangesAndUndoLogInternal(false);
DCHECK(!s.IsNotFound());
if (UNLIKELY(!s.ok() && !s.IsNotFound()))
return s;
leveldb::ReadOptions options;
options.verify_checksums = true;
// Since these are keys that are being deleted, this should not fill the
// block cache (as the data will be immediately stale).
options.fill_cache = false;
const std::unique_ptr<leveldb::Iterator> it =
base::WrapUnique(level_db_->db()->NewIterator(options));
const leveldb::Comparator* comparator = level_db_->comparator();
for (it->Seek(begin);
(s = it->status(), s.ok()) && it->Valid() &&
IsKeyBeforeEndOfRange(comparator, it->key(), end, end_exclusive);
it->Next()) {
// To avoid setting mode to UndoLog if there are no keys to delete, call the
// function here inside of the loop.
SetModeToUndoLog();
// Undo log.
AddUndoPutTask(it->key().ToString(), it->value().ToString());
// Removal.
buffer_batch_.Delete(it->key());
buffer_batch_empty_ = false;
// Make sure our buffer batch isn't getting too big.
if (GetMemoryUsage() > write_batch_size_) {
s = WriteBufferBatch(false);
DCHECK(!s.IsNotFound());
if (UNLIKELY(!s.ok() && !s.IsNotFound()))
return s;
}
}
if (UNLIKELY(!s.ok() && !s.IsNotFound()))
return s;
// This could happen if there were no keys found in the range.
if (buffer_batch_empty_)
return leveldb::Status::OK();
return WriteBufferBatch(false);
}
leveldb::Status LevelDBScope::WriteChangesAndUndoLog() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return WriteChangesAndUndoLogInternal(false);
}
leveldb::Status LevelDBScope::Rollback() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!committed_);
DCHECK(rollback_callback_);
if (!has_written_to_disk_) {
buffer_batch_.Clear();
buffer_batch_empty_ = true;
return leveldb::Status::OK();
}
DCHECK(undo_sequence_number_ < std::numeric_limits<int64_t>::max() ||
cleanup_sequence_number_ > 0)
<< "A reverting scope that has written to disk must have either an "
"undo or cleanup task written to it. undo_sequence_number_: "
<< undo_sequence_number_
<< ", cleanup_sequence_number_: " << cleanup_sequence_number_;
return std::move(rollback_callback_).Run(scope_id_, std::move(locks_));
}
std::pair<leveldb::Status, LevelDBScope::Mode> LevelDBScope::Commit(
bool sync_on_commit) {
DCHECK(!locks_.empty());
DCHECK(!committed_);
DCHECK(rollback_callback_);
leveldb::Status s;
switch (mode_) {
case Mode::kInMemory:
// Don't bother hitting disk if we don't have anything.
if (!buffer_batch_empty_)
s = WriteBufferBatch(sync_on_commit);
break;
case Mode::kUndoLogOnDisk:
AddCommitPoint();
s = WriteChangesAndUndoLogInternal(sync_on_commit);
break;
default:
NOTREACHED();
return {leveldb::Status::NotSupported("Unknown scopes mode."), mode_};
}
locks_.clear();
committed_ = true;
return {s, mode_};
}
leveldb::Status LevelDBScope::WriteChangesAndUndoLogInternal(bool sync) {
if (buffer_batch_empty_)
return leveldb::Status::OK();
SetModeToUndoLog();
leveldb::WriteBatch changes = buffer_batch_;
UndoLogWriter undo_writer(this, level_db_->db());
changes.Iterate(&undo_writer);
changes.Clear();
return WriteBufferBatch(sync);
}
void LevelDBScope::AddUndoPutTask(std::string key, std::string value) {
DCHECK(undo_task_buffer_.operation_case() ==
LevelDBScopesUndoTask::OPERATION_NOT_SET);
auto* const put = undo_task_buffer_.mutable_put();
put->set_key(std::move(key));
put->set_value(std::move(value));
AddBufferedUndoTask();
}
void LevelDBScope::AddUndoDeleteTask(std::string key) {
DCHECK(undo_task_buffer_.operation_case() ==
LevelDBScopesUndoTask::OPERATION_NOT_SET);
auto* const del = undo_task_buffer_.mutable_delete_();
del->set_key(std::move(key));
AddBufferedUndoTask();
}
void LevelDBScope::AddUndoDeleteRangeTask(std::string begin, std::string end) {
DCHECK(undo_task_buffer_.operation_case() ==
LevelDBScopesUndoTask::OPERATION_NOT_SET);
auto* const range = undo_task_buffer_.mutable_delete_range();
range->set_begin(std::move(begin));
range->set_end(std::move(end));
AddBufferedUndoTask();
}
void LevelDBScope::AddBufferedUndoTask() {
undo_task_buffer_.SerializeToString(&value_buffer_);
buffer_batch_.Put(
key_encoder_.UndoTaskKey(prefix_, scope_id_, undo_sequence_number_),
value_buffer_);
DCHECK_GT(cleanup_sequence_number_, std::numeric_limits<int64_t>::min());
--undo_sequence_number_;
buffer_batch_empty_ = false;
undo_task_buffer_.Clear();
}
void LevelDBScope::AddCleanupDeleteRangeTask(std::string begin,
std::string end) {
DCHECK(cleanup_task_buffer_.operation_case() ==
LevelDBScopesCleanupTask::OPERATION_NOT_SET);
auto* const range = cleanup_task_buffer_.mutable_delete_range();
range->set_begin(std::move(begin));
range->set_end(std::move(end));
AddBufferedCleanupTask();
}
void LevelDBScope::AddCleanupDeleteAndCompactRangeTask(std::string begin,
std::string end) {
DCHECK(cleanup_task_buffer_.operation_case() ==
LevelDBScopesCleanupTask::OPERATION_NOT_SET);
auto* const range = cleanup_task_buffer_.mutable_delete_range_and_compact();
range->set_begin(std::move(begin));
range->set_end(std::move(end));
AddBufferedCleanupTask();
}
void LevelDBScope::AddBufferedCleanupTask() {
cleanup_task_buffer_.SerializeToString(&value_buffer_);
buffer_batch_.Put(
key_encoder_.CleanupTaskKey(prefix_, scope_id_, cleanup_sequence_number_),
value_buffer_);
DCHECK_LT(cleanup_sequence_number_, std::numeric_limits<int64_t>::max());
++cleanup_sequence_number_;
buffer_batch_empty_ = false;
cleanup_task_buffer_.Clear();
}
void LevelDBScope::SetModeToUndoLog() {
if (mode_ == Mode::kUndoLogOnDisk)
return;
mode_ = Mode::kUndoLogOnDisk;
LevelDBScopesScopeMetadata metadata;
for (ScopeLock& lock : locks_) {
auto* lock_proto = metadata.add_locks();
lock_proto->set_level(lock.level());
auto* range = lock_proto->mutable_range();
range->set_begin(lock.range().begin);
range->set_end(lock.range().end);
}
metadata.SerializeToString(&value_buffer_);
buffer_batch_.Put(key_encoder_.ScopeMetadataKey(prefix_, scope_id_),
value_buffer_);
buffer_batch_empty_ = false;
}
bool LevelDBScope::CanSkipWritingUndoEntry(const leveldb::Slice& key) {
const leveldb::Comparator* const comparator = level_db_->comparator();
if (key.starts_with(leveldb::Slice(
reinterpret_cast<const char*>(prefix_.data()), prefix_.size())))
return true;
const auto it = std::upper_bound(
empty_ranges_.begin(), empty_ranges_.end(), key,
[comparator](const leveldb::Slice& key,
const std::pair<EmptyRange, bool>& range) {
// Compare the key to the end of the range.
const EmptyRange& empty_range = range.first;
return comparator->Compare(key, empty_range.second) < 0;
});
// If the key wasn't found (iterator is at the end, or the key is before the
// beginning).
if (LIKELY(it == empty_ranges_.end() ||
comparator->Compare(key, it->first.first) < 0)) {
return false;
}
// The key is within an empty range.
if (!it->second) {
// Only add the delete range once.
AddUndoDeleteRangeTask(it->first.first, it->first.second);
it->second = true;
}
return true;
}
void LevelDBScope::AddCommitPoint() {
DCHECK(mode_ == Mode::kUndoLogOnDisk);
// Remove the lock ranges from the metadata, which is the 'commit point' and
// means that this scope is committed.
LevelDBScopesScopeMetadata metadata;
metadata.SerializeToString(&value_buffer_);
buffer_batch_.Put(key_encoder_.ScopeMetadataKey(prefix_, scope_id_),
value_buffer_);
buffer_batch_empty_ = false;
}
leveldb::Status LevelDBScope::WriteBufferBatch(bool sync) {
leveldb::WriteOptions write_options;
write_options.sync = sync;
approximate_bytes_written_ += buffer_batch_.ApproximateSize();
leveldb::Status s = level_db_->db()->Write(write_options, &buffer_batch_);
// We intentionally clear the write batch, even if the write fails, as this
// class is expected to be treated as invalid after a failure and shouldn't be
// used.
buffer_batch_.Clear();
has_written_to_disk_ = true;
buffer_batch_empty_ = true;
return s;
}
#if DCHECK_IS_ON()
bool LevelDBScope::IsRangeEmpty(const EmptyRange& range) {
leveldb::ReadOptions read_options;
read_options.verify_checksums = true;
// This is a debug-only operation, so it shouldn't fill the cache.
read_options.fill_cache = false;
const std::unique_ptr<leveldb::Iterator> it =
base::WrapUnique(level_db_->db()->NewIterator(read_options));
const leveldb::Comparator* const comparator = level_db_->comparator();
it->Seek(range.first);
DCHECK(!it->Valid() || it->status().ok())
<< "leveldb::Iterator::Valid() should imply an OK status";
return !it->Valid() ||
!IsKeyBeforeEndOfRange(comparator, it->key(), range.second, true);
}
bool LevelDBScope::IsInDeferredDeletionRange(const leveldb::Slice& key) {
const leveldb::Comparator* comparator = level_db_->comparator();
for (const auto& range : deferred_delete_ranges_) {
int beginCompare = comparator->Compare(range.first, key);
if (beginCompare > 0)
continue;
if (beginCompare == 0)
return true;
int endCompare = comparator->Compare(range.second, key);
if (endCompare < 0)
continue;
return endCompare > 0;
}
return false;
}
void LevelDBScope::ValidateEmptyRanges() {
for (auto it = empty_ranges_.begin(); it != empty_ranges_.end(); ++it) {
auto range = it->first;
DCHECK(IsRangeEmpty(range))
<< "Range [" << range.first << ", " << range.second
<< ") was reported empty, but keys were found.";
auto next_it = it;
++next_it;
if (next_it != empty_ranges_.end()) {
DCHECK(level_db_->comparator()->Compare(range.second,
next_it->first.first) <= 0)
<< "The |empty_ranges| are not disjoint.";
}
auto last_it = it;
if (last_it != empty_ranges_.begin()) {
--last_it;
DCHECK(level_db_->comparator()->Compare(last_it->first.second,
range.first) <= 0)
<< "The |empty_ranges| are not disjoint.";
}
}
}
#endif
} // namespace content